2018-12-07 13:10:52 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2018-12-07 13:10:52 +00:00
|
|
|
*
|
|
|
|
|
* 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';
|
|
|
|
|
|
2019-03-20 13:47:58 +00:00
|
|
|
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
|
|
|
|
import {Reference} from '../../../src/ngtsc/imports';
|
2020-05-06 11:02:54 +01:00
|
|
|
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
|
|
|
|
import {ClassDeclaration, isNamedClassDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
2020-05-05 10:19:28 +01:00
|
|
|
import {NgccReflectionHost} from '../host/ngcc_host';
|
2019-03-05 23:29:28 +01:00
|
|
|
import {hasNameIdentifier, isDefined} from '../utils';
|
2018-12-07 13:10:52 +00:00
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
/**
|
|
|
|
|
* A structure returned from `getModuleWithProvidersFunctions()` that describes functions
|
|
|
|
|
* that return ModuleWithProviders objects.
|
|
|
|
|
*/
|
2018-12-07 13:10:52 +00:00
|
|
|
export interface ModuleWithProvidersInfo {
|
|
|
|
|
/**
|
2020-05-06 11:02:54 +01:00
|
|
|
* 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)
|
2018-12-07 13:10:52 +00:00
|
|
|
*/
|
2020-05-06 11:02:54 +01:00
|
|
|
container: ts.Declaration|null;
|
2018-12-07 13:10:52 +00:00
|
|
|
/**
|
2020-05-06 11:02:54 +01:00
|
|
|
* The declaration of the class that the `ngModule` property on the `ModuleWithProviders` object
|
|
|
|
|
* refers to.
|
2018-12-07 13:10:52 +00:00
|
|
|
*/
|
2020-05-06 11:02:54 +01:00
|
|
|
ngModule: Reference<ClassDeclaration>;
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
|
|
|
|
|
export const ModuleWithProvidersAnalyses = Map;
|
|
|
|
|
|
|
|
|
|
export class ModuleWithProvidersAnalyzer {
|
2020-05-06 11:02:54 +01:00
|
|
|
private evaluator = new PartialEvaluator(this.host, this.typeChecker, null);
|
|
|
|
|
|
2019-11-16 21:12:58 +01:00
|
|
|
constructor(
|
2020-05-06 11:02:54 +01:00
|
|
|
private host: NgccReflectionHost, private typeChecker: ts.TypeChecker,
|
|
|
|
|
private referencesRegistry: ReferencesRegistry, private processDts: boolean) {}
|
2018-12-07 13:10:52 +00:00
|
|
|
|
|
|
|
|
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
|
2020-05-06 11:02:54 +01:00
|
|
|
const analyses: ModuleWithProvidersAnalyses = new ModuleWithProvidersAnalyses();
|
2018-12-07 13:10:52 +00:00
|
|
|
const rootFiles = this.getRootFiles(program);
|
|
|
|
|
rootFiles.forEach(f => {
|
2020-05-05 10:19:28 +01:00
|
|
|
const fns = this.getModuleWithProvidersFunctions(f);
|
2018-12-07 13:10:52 +00:00
|
|
|
fns && fns.forEach(fn => {
|
2020-05-06 11:02:54 +01:00
|
|
|
if (fn.ngModule.bestGuessOwningModule === null) {
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 21:12:58 +01:00
|
|
|
// Only when processing the dts files do we need to determine which declaration to update.
|
|
|
|
|
if (this.processDts) {
|
2020-05-06 11:02:54 +01:00
|
|
|
const dtsFn = this.getDtsModuleWithProvidersFunction(fn);
|
|
|
|
|
const dtsFnType = dtsFn.declaration.type;
|
|
|
|
|
const typeParam = dtsFnType && ts.isTypeReferenceNode(dtsFnType) &&
|
|
|
|
|
dtsFnType.typeArguments && dtsFnType.typeArguments[0] ||
|
2019-11-16 21:12:58 +01:00
|
|
|
null;
|
|
|
|
|
if (!typeParam || isAnyKeyword(typeParam)) {
|
2020-05-06 11:02:54 +01:00
|
|
|
const dtsFile = dtsFn.declaration.getSourceFile();
|
|
|
|
|
const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile)! : [];
|
|
|
|
|
analysis.push(dtsFn);
|
2019-11-16 21:12:58 +01:00
|
|
|
analyses.set(dtsFile, analysis);
|
|
|
|
|
}
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return analyses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getRootFiles(program: ts.Program): ts.SourceFile[] {
|
|
|
|
|
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
private getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersInfo[] {
|
2020-05-05 10:19:28 +01:00
|
|
|
const exports = this.host.getExportsOfModule(f);
|
|
|
|
|
if (!exports) return [];
|
2020-05-06 11:02:54 +01:00
|
|
|
const infos: ModuleWithProvidersInfo[] = [];
|
|
|
|
|
exports.forEach((declaration) => {
|
2020-05-05 10:19:28 +01:00
|
|
|
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,
|
2020-05-06 11:02:54 +01:00
|
|
|
container: ts.Declaration|null = null): ModuleWithProvidersInfo|null {
|
2020-05-05 10:19:28 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2020-05-06 11:02:54 +01:00
|
|
|
|
2020-05-05 10:19:28 +01:00
|
|
|
const body = definition.body;
|
2020-05-06 11:02:54 +01:00
|
|
|
if (body === null || body.length === 0) {
|
2020-05-05 10:19:28 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
// 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;
|
2020-05-05 10:19:28 +01:00
|
|
|
}
|
|
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
// Evaluate this expression and extract the `ngModule` reference
|
|
|
|
|
const result = this.evaluator.evaluate(lastStatement.expression);
|
|
|
|
|
if (!(result instanceof Map) || !result.has('ngModule')) {
|
2020-05-05 10:19:28 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
const ngModuleRef = result.get('ngModule')!;
|
|
|
|
|
if (!(ngModuleRef instanceof Reference)) {
|
2020-05-05 10:19:28 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
2020-05-06 11:02:54 +01:00
|
|
|
|
|
|
|
|
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};
|
2020-05-05 10:19:28 +01:00
|
|
|
}
|
|
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
private getDtsModuleWithProvidersFunction(fn: ModuleWithProvidersInfo): ModuleWithProvidersInfo {
|
2018-12-07 13:10:52 +00:00
|
|
|
let dtsFn: ts.Declaration|null = null;
|
2019-03-20 13:47:58 +00:00
|
|
|
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);
|
2018-12-07 13:10:52 +00:00
|
|
|
// Get the declaration of the matching static method
|
|
|
|
|
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
2020-04-06 08:30:08 +01:00
|
|
|
dtsClass.members.find(
|
|
|
|
|
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
|
|
|
|
member.name.text === fn.name) as ts.Declaration :
|
2018-12-07 13:10:52 +00:00
|
|
|
null;
|
|
|
|
|
} else {
|
2019-03-20 13:47:58 +00:00
|
|
|
dtsFn = this.host.getDtsDeclaration(fn.declaration);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
if (!dtsFn) {
|
2019-03-20 13:47:58 +00:00
|
|
|
throw new Error(`Matching type declaration for ${fn.declaration.getText()} is missing`);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
if (!isFunctionOrMethod(dtsFn)) {
|
2020-04-06 08:30:08 +01:00
|
|
|
throw new Error(`Matching type declaration for ${
|
|
|
|
|
fn.declaration.getText()} is not a function: ${dtsFn.getText()}`);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
2020-05-06 11:02:54 +01:00
|
|
|
const container = containerClass ? containerClass.declaration.valueDeclaration : null;
|
|
|
|
|
const ngModule = this.resolveNgModuleReference(fn);
|
|
|
|
|
return {name: fn.name, container, declaration: dtsFn, ngModule};
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
2019-09-29 23:26:41 +02:00
|
|
|
|
2020-05-06 11:02:54 +01:00
|
|
|
private resolveNgModuleReference(fn: ModuleWithProvidersInfo): Reference<ClassDeclaration> {
|
2019-09-29 23:26:41 +02:00
|
|
|
const ngModule = fn.ngModule;
|
|
|
|
|
|
|
|
|
|
// For external module references, use the declaration as is.
|
2020-05-06 11:02:54 +01:00
|
|
|
if (ngModule.bestGuessOwningModule !== null) {
|
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) {
|
2020-04-06 08:30:08 +01:00
|
|
|
throw new Error(`No typings declaration can be found for the referenced NgModule class in ${
|
|
|
|
|
fn.declaration.getText()}.`);
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
2020-05-06 11:02:54 +01:00
|
|
|
if (!isNamedClassDeclaration(dtsNgModule)) {
|
2020-04-06 08:30:08 +01:00
|
|
|
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()}`);
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
2020-05-06 11:02:54 +01:00
|
|
|
return new Reference(dtsNgModule, null);
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
2018-12-07 13:10:52 +00: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;
|
|
|
|
|
}
|