angular-cn/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts

232 lines
8.9 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
import {Reference} from '../../../src/ngtsc/imports';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, isNamedClassDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils';
/**
* A structure returned from `getModuleWithProvidersFunctions()` that describes functions
* that return ModuleWithProviders objects.
*/
export interface ModuleWithProvidersInfo {
/**
* The name of the declared function.
*/
name: string;
/**
* The declaration of the function that returns the `ModuleWithProviders` object.
*/
declaration: ts.SignatureDeclaration;
/**
* Declaration of the containing class (if this is a method)
*/
container: ts.Declaration|null;
/**
* The declaration of the class that the `ngModule` property on the `ModuleWithProviders` object
* refers to.
*/
ngModule: Reference<ClassDeclaration>;
}
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
export const ModuleWithProvidersAnalyses = Map;
export class ModuleWithProvidersAnalyzer {
private evaluator = new PartialEvaluator(this.host, this.typeChecker, null);
constructor(
private host: NgccReflectionHost, private typeChecker: ts.TypeChecker,
private referencesRegistry: ReferencesRegistry, private processDts: boolean) {}
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
const analyses: ModuleWithProvidersAnalyses = new ModuleWithProvidersAnalyses();
const rootFiles = this.getRootFiles(program);
rootFiles.forEach(f => {
const fns = this.getModuleWithProvidersFunctions(f);
fns && fns.forEach(fn => {
if (fn.ngModule.bestGuessOwningModule === null) {
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
// Record the usage of an internal module as it needs to become an exported symbol
this.referencesRegistry.add(fn.ngModule.node, new Reference(fn.ngModule.node));
}
// Only when processing the dts files do we need to determine which declaration to update.
if (this.processDts) {
const dtsFn = this.getDtsModuleWithProvidersFunction(fn);
const dtsFnType = dtsFn.declaration.type;
const typeParam = dtsFnType && ts.isTypeReferenceNode(dtsFnType) &&
dtsFnType.typeArguments && dtsFnType.typeArguments[0] ||
null;
if (!typeParam || isAnyKeyword(typeParam)) {
const dtsFile = dtsFn.declaration.getSourceFile();
const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile)! : [];
analysis.push(dtsFn);
analyses.set(dtsFile, analysis);
}
}
});
});
return analyses;
}
private getRootFiles(program: ts.Program): ts.SourceFile[] {
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
}
private getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersInfo[] {
const exports = this.host.getExportsOfModule(f);
if (!exports) return [];
const infos: ModuleWithProvidersInfo[] = [];
exports.forEach((declaration) => {
if (declaration.node === null) {
return;
}
if (this.host.isClass(declaration.node)) {
this.host.getMembersOfClass(declaration.node).forEach(member => {
if (member.isStatic) {
const info = this.parseForModuleWithProviders(
member.name, member.node, member.implementation, declaration.node);
if (info) {
infos.push(info);
}
}
});
} else {
if (hasNameIdentifier(declaration.node)) {
const info =
this.parseForModuleWithProviders(declaration.node.name.text, declaration.node);
if (info) {
infos.push(info);
}
}
}
});
return infos;
}
/**
* Parse a function/method node (or its implementation), to see if it returns a
* `ModuleWithProviders` object.
* @param name The name of the function.
* @param node the node to check - this could be a function, a method or a variable declaration.
* @param implementation the actual function expression if `node` is a variable declaration.
* @param container the class that contains the function, if it is a method.
* @returns info about the function if it does return a `ModuleWithProviders` object; `null`
* otherwise.
*/
private parseForModuleWithProviders(
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
container: ts.Declaration|null = null): ModuleWithProvidersInfo|null {
if (implementation === null ||
(!ts.isFunctionDeclaration(implementation) && !ts.isMethodDeclaration(implementation) &&
!ts.isFunctionExpression(implementation))) {
return null;
}
const declaration = implementation;
const definition = this.host.getDefinitionOfFunction(declaration);
if (definition === null) {
return null;
}
const body = definition.body;
if (body === null || body.length === 0) {
return null;
}
// Get hold of the return statement expression for the function
const lastStatement = body[body.length - 1];
if (!ts.isReturnStatement(lastStatement) || lastStatement.expression === undefined) {
return null;
}
// Evaluate this expression and extract the `ngModule` reference
const result = this.evaluator.evaluate(lastStatement.expression);
if (!(result instanceof Map) || !result.has('ngModule')) {
return null;
}
const ngModuleRef = result.get('ngModule')!;
if (!(ngModuleRef instanceof Reference)) {
return null;
}
if (!isNamedClassDeclaration(ngModuleRef.node) &&
!isNamedVariableDeclaration(ngModuleRef.node)) {
throw new Error(`The identity given by ${ngModuleRef.debugName} referenced in "${
declaration!.getText()}" doesn't appear to be a "class" declaration.`);
}
const ngModule = ngModuleRef as Reference<ClassDeclaration>;
return {name, ngModule, declaration, container};
}
private getDtsModuleWithProvidersFunction(fn: ModuleWithProvidersInfo): ModuleWithProvidersInfo {
let dtsFn: ts.Declaration|null = null;
const containerClass = fn.container && this.host.getClassSymbol(fn.container);
if (containerClass) {
fix(ngcc): consistently use outer declaration for classes (#32539) In ngcc's reflection hosts for compiled JS bundles, such as ESM2015, special care needs to be taken for classes as there may be an outer declaration (referred to as "declaration") and an inner declaration (referred to as "implementation") for a given class. Therefore, there will also be two `ts.Symbol`s bound per class, and ngcc needs to switch between those declarations and symbols depending on where certain information can be found. Prior to this commit, the `NgccReflectionHost` interface had methods `getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`. These class symbols would be used to kick off compilation of components using ngtsc, so it is important for these symbols to correspond with the publicly visible outer declaration of the class. However, the ESM2015 reflection host used to return the `ts.Symbol` for the inner declaration, if the class was declared as follows: ```javascript var MyClass = class MyClass {}; ``` For the above code, `Esm2015ReflectionHost.getClassSymbol` would return the `ts.Symbol` corresponding with the `class MyClass {}` declaration, whereas it should have corresponded with the `var MyClass` declaration. As a consequence, no `NgModule` could be resolved for the component, so no components/directives would be in scope for the component. This resulted in errors during runtime. This commit resolves the issue by introducing a `NgccClassSymbol` that contains references to both the outer and inner `ts.Symbol`, instead of just a single `ts.Symbol`. This avoids the unclarity of whether a `ts.Symbol` corresponds with the outer or inner declaration. More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw Fixes #32078 Closes FW-1507 PR Close #32539
2019-09-03 21:26:58 +02:00
const dtsClass = this.host.getDtsDeclaration(containerClass.declaration.valueDeclaration);
// Get the declaration of the matching static method
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
dtsClass.members.find(
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
member.name.text === fn.name) as ts.Declaration :
null;
} else {
dtsFn = this.host.getDtsDeclaration(fn.declaration);
}
if (!dtsFn) {
throw new Error(`Matching type declaration for ${fn.declaration.getText()} is missing`);
}
if (!isFunctionOrMethod(dtsFn)) {
throw new Error(`Matching type declaration for ${
fn.declaration.getText()} is not a function: ${dtsFn.getText()}`);
}
const container = containerClass ? containerClass.declaration.valueDeclaration : null;
const ngModule = this.resolveNgModuleReference(fn);
return {name: fn.name, container, declaration: dtsFn, ngModule};
}
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
private resolveNgModuleReference(fn: ModuleWithProvidersInfo): Reference<ClassDeclaration> {
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
const ngModule = fn.ngModule;
// For external module references, use the declaration as is.
if (ngModule.bestGuessOwningModule !== null) {
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
return ngModule;
}
// For internal (non-library) module references, redirect the module's value declaration
// to its type declaration.
const dtsNgModule = this.host.getDtsDeclaration(ngModule.node);
if (!dtsNgModule) {
throw new Error(`No typings declaration can be found for the referenced NgModule class in ${
fn.declaration.getText()}.`);
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
}
if (!isNamedClassDeclaration(dtsNgModule)) {
throw new Error(`The referenced NgModule in ${
fn.declaration
.getText()} is not a named class declaration in the typings program; instead we get ${
dtsNgModule.getText()}`);
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
}
return new Reference(dtsNgModule, null);
fix(ngcc): ensure private exports are added for `ModuleWithProviders` (#32902) ngcc may need to insert public exports into the bundle's source as well as to the entry-point's declaration file, as the Ivy compiler may need to create import statements to internal library types. The way ngcc knows which exports to add is through the references registry, to which references to things that require a public export are added by the various analysis steps that are executed. One of these analysis steps is the augmentation of declaration files where functions that return `ModuleWithProviders` are updated so that a generic type argument is added that corresponds with the `NgModule` that is actually imported. This type has to be publicly exported, so the analyzer step has to add the module type to the references registry. A problem occurs when `ModuleWithProviders` already has a generic type argument, in which case no update of the declaration file is necessary. This may happen when 1) ngcc is processing additional bundle formats, so that the declaration file has already been updated while processing the first bundle format, or 2) when a package is processed which already contains the generic type in its source. In both scenarios it may occur that the referenced `NgModule` type does not yet have a public export, so it is crucial that a reference to the type is added to the references registry, which ngcc failed to do. This commit fixes the issue by always adding the referenced `NgModule` type to the references registry, so that a public export will always be created if necessary. Resolves FW-1575 PR Close #32902
2019-09-29 23:26:41 +02:00
}
}
function isFunctionOrMethod(declaration: ts.Declaration): declaration is ts.FunctionDeclaration|
ts.MethodDeclaration {
return ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration);
}
function isAnyKeyword(typeParam: ts.TypeNode): typeParam is ts.KeywordTypeNode {
return typeParam.kind === ts.SyntaxKind.AnyKeyword;
}