fix(ivy): ngcc - properly handle aliases class expressions (#29119)
In ES2015, classes could have been emitted as a variable declaration initialized with a class expression. In certain situations, an intermediary variable suffixed with `_1` is present such that the variable declaration's initializer becomes a binary expression with its rhs being the class expression, and its lhs being the identifier of the intermediate variable. This structure was not recognized, resulting in such classes not being considered as a class in `Esm2015ReflectionHost`. As a consequence, the analysis of functions/methods that return a `ModuleWithProviders` object did not take the methods of such classes into account. Another edge-case with such intermediate variable was that static properties would not be considered as class members. A testcase was added to prevent regressions. Fixes #29078 PR Close #29119
This commit is contained in:
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {BundleProgram} from '../packages/bundle_program';
|
||||
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
|
||||
@ -50,6 +50,29 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
|
||||
*/
|
||||
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
|
||||
protected dtsDeclarationMap: Map<string, ts.Declaration>|null;
|
||||
|
||||
/**
|
||||
* The set of source files that have already been preprocessed.
|
||||
*/
|
||||
protected preprocessedSourceFiles = new Set<ts.SourceFile>();
|
||||
|
||||
/**
|
||||
* In ES2015, class declarations may have been down-leveled into variable declarations,
|
||||
* initialized using a class expression. In certain scenarios, an additional variable
|
||||
* is introduced that represents the class so that results in code such as:
|
||||
*
|
||||
* ```
|
||||
* let MyClass_1; let MyClass = MyClass_1 = class MyClass {};
|
||||
* ```
|
||||
*
|
||||
* This map tracks those aliased variables to their original identifier, i.e. the key
|
||||
* corresponds with the declaration of `MyClass_1` and its value becomes the `MyClass` identifier
|
||||
* of the variable declaration.
|
||||
*
|
||||
* This map is populated during the preprocessing of each source file.
|
||||
*/
|
||||
protected aliasedClassDeclarations = new Map<ts.Declaration, ts.Identifier>();
|
||||
|
||||
constructor(
|
||||
protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker,
|
||||
dts?: BundleProgram|null) {
|
||||
@ -62,19 +85,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
* Classes should have a `name` identifier, because they may need to be referenced in other parts
|
||||
* of the program.
|
||||
*
|
||||
* In ES2015, a class may be declared using a variable declaration of the following structure:
|
||||
*
|
||||
* ```
|
||||
* var MyClass = MyClass_1 = class MyClass {};
|
||||
* ```
|
||||
*
|
||||
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
|
||||
* `class MyClass {}` node is returned as declaration of `MyClass`.
|
||||
*
|
||||
* @param node the node that represents the class whose declaration we are finding.
|
||||
* @returns the declaration of the class or `undefined` if it is not a "class".
|
||||
*/
|
||||
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
node = node.initializer;
|
||||
}
|
||||
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return hasNameIdentifier(node) ? node : undefined;
|
||||
return getInnerClassDeclaration(node) || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,6 +179,60 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
return null;
|
||||
}
|
||||
|
||||
hasBaseClass(clazz: ClassDeclaration): boolean {
|
||||
const superHasBaseClass = super.hasBaseClass(clazz);
|
||||
if (superHasBaseClass) {
|
||||
return superHasBaseClass;
|
||||
}
|
||||
|
||||
const innerClassDeclaration = getInnerClassDeclaration(clazz);
|
||||
if (innerClassDeclaration === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return innerClassDeclaration.heritageClauses !== undefined &&
|
||||
innerClassDeclaration.heritageClauses.some(
|
||||
clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given node actually represents a class.
|
||||
*/
|
||||
isClass(node: ts.Node): node is ClassDeclaration {
|
||||
return super.isClass(node) || !!this.getClassDeclaration(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trace an identifier to its declaration, if possible.
|
||||
*
|
||||
* This method attempts to resolve the declaration of the given identifier, tracing back through
|
||||
* imports and re-exports until the original declaration statement is found. A `Declaration`
|
||||
* object is returned if the original declaration is found, or `null` is returned otherwise.
|
||||
*
|
||||
* In ES2015, we need to account for identifiers that refer to aliased class declarations such as
|
||||
* `MyClass_1`. Since such declarations are only available within the module itself, we need to
|
||||
* find the original class declaration, e.g. `MyClass`, that is associated with the aliased one.
|
||||
*
|
||||
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
|
||||
*
|
||||
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
|
||||
* otherwise.
|
||||
*/
|
||||
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
||||
const superDeclaration = super.getDeclarationOfIdentifier(id);
|
||||
|
||||
// The identifier may have been of an additional class assignment such as `MyClass_1` that was
|
||||
// present as alias for `MyClass`. If so, resolve such aliases to their original declaration.
|
||||
if (superDeclaration !== null) {
|
||||
const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node);
|
||||
if (aliasedIdentifier !== null) {
|
||||
return this.getDeclarationOfIdentifier(aliasedIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
return superDeclaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the given module for variable declarations in which the initializer
|
||||
* is an identifier marked with the `PRE_R3_MARKER`.
|
||||
@ -341,6 +419,74 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
|
||||
///////////// Protected Helpers /////////////
|
||||
|
||||
/**
|
||||
* Finds the identifier of the actual class declaration for a potentially aliased declaration of a
|
||||
* class.
|
||||
*
|
||||
* If the given declaration is for an alias of a class, this function will determine an identifier
|
||||
* to the original declaration that represents this class.
|
||||
*
|
||||
* @param declaration The declaration to resolve.
|
||||
* @returns The original identifier that the given class declaration resolves to, or `undefined`
|
||||
* if the declaration does not represent an aliased class.
|
||||
*/
|
||||
protected resolveAliasedClassIdentifier(declaration: ts.Declaration): ts.Identifier|null {
|
||||
this.ensurePreprocessed(declaration.getSourceFile());
|
||||
return this.aliasedClassDeclarations.get(declaration) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the source file that `node` is part of has been preprocessed.
|
||||
*
|
||||
* During preprocessing, all statements in the source file will be visited such that certain
|
||||
* processing steps can be done up-front and cached for subsequent usages.
|
||||
*
|
||||
* @param sourceFile The source file that needs to have gone through preprocessing.
|
||||
*/
|
||||
protected ensurePreprocessed(sourceFile: ts.SourceFile): void {
|
||||
if (!this.preprocessedSourceFiles.has(sourceFile)) {
|
||||
this.preprocessedSourceFiles.add(sourceFile);
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
this.preprocessStatement(statement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes the given statement to see if it corresponds with a variable declaration like
|
||||
* `let MyClass = MyClass_1 = class MyClass {};`. If so, the declaration of `MyClass_1`
|
||||
* is associated with the `MyClass` identifier.
|
||||
*
|
||||
* @param statement The statement that needs to be preprocessed.
|
||||
*/
|
||||
protected preprocessStatement(statement: ts.Statement): void {
|
||||
if (!ts.isVariableStatement(statement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declarations = statement.declarationList.declarations;
|
||||
if (declarations.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declaration = declarations[0];
|
||||
const initializer = declaration.initializer;
|
||||
if (!ts.isIdentifier(declaration.name) || !initializer || !isAssignment(initializer) ||
|
||||
!ts.isIdentifier(initializer.left) || !ts.isClassExpression(initializer.right)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aliasedIdentifier = initializer.left;
|
||||
|
||||
const aliasedDeclaration = this.getDeclarationOfIdentifier(aliasedIdentifier);
|
||||
if (aliasedDeclaration === null) {
|
||||
throw new Error(
|
||||
`Unable to locate declaration of ${aliasedIdentifier.text} in "${statement.getText()}"`);
|
||||
}
|
||||
this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name);
|
||||
}
|
||||
|
||||
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
|
||||
if (decoratorsProperty) {
|
||||
@ -492,8 +638,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
// }
|
||||
// MyClass.staticProperty = ...;
|
||||
// ```
|
||||
if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) {
|
||||
const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name);
|
||||
const variableDeclaration = getVariableDeclarationOfDeclaration(symbol.valueDeclaration);
|
||||
if (variableDeclaration !== undefined) {
|
||||
const variableSymbol = this.checker.getSymbolAtLocation(variableDeclaration.name);
|
||||
if (variableSymbol && variableSymbol.exports) {
|
||||
variableSymbol.exports.forEach((value, key) => {
|
||||
const decorators = decoratorsMap.get(key as string);
|
||||
@ -703,7 +850,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reflect over the given array node and extract decorator information from each element.
|
||||
*
|
||||
@ -1166,12 +1312,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
protected parseForModuleWithProviders(
|
||||
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
|
||||
container: ts.Declaration|null = null): ModuleWithProvidersFunction|null {
|
||||
const declaration = implementation &&
|
||||
(ts.isFunctionDeclaration(implementation) || ts.isMethodDeclaration(implementation) ||
|
||||
ts.isFunctionExpression(implementation)) ?
|
||||
implementation :
|
||||
null;
|
||||
const body = declaration ? this.getDefinitionOfFunction(declaration).body : null;
|
||||
if (implementation === null ||
|
||||
(!ts.isFunctionDeclaration(implementation) && !ts.isMethodDeclaration(implementation) &&
|
||||
!ts.isFunctionExpression(implementation))) {
|
||||
return null;
|
||||
}
|
||||
const declaration = implementation;
|
||||
const body = this.getDefinitionOfFunction(declaration).body;
|
||||
const lastStatement = body && body[body.length - 1];
|
||||
const returnExpression =
|
||||
lastStatement && ts.isReturnStatement(lastStatement) && lastStatement.expression || null;
|
||||
@ -1180,10 +1327,27 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||
prop =>
|
||||
!!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') ||
|
||||
null;
|
||||
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
|
||||
const ngModuleIdentifier = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
|
||||
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
|
||||
null;
|
||||
return ngModule && declaration && {name, ngModule, declaration, container};
|
||||
|
||||
// If no `ngModule` property was found in an object literal return value, return `null` to
|
||||
// indicate that the provided node does not appear to be a `ModuleWithProviders` function.
|
||||
if (ngModuleIdentifier === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ngModuleDeclaration = this.getDeclarationOfIdentifier(ngModuleIdentifier);
|
||||
if (!ngModuleDeclaration) {
|
||||
throw new Error(
|
||||
`Cannot find a declaration for NgModule ${ngModuleIdentifier.text} referenced in "${declaration!.getText()}"`);
|
||||
}
|
||||
if (!hasNameIdentifier(ngModuleDeclaration.node)) {
|
||||
return null;
|
||||
}
|
||||
const ngModule = ngModuleDeclaration as Declaration<ClassDeclaration>;
|
||||
|
||||
return {name, ngModule, declaration, container};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1209,10 +1373,8 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass
|
||||
ts.isIdentifier(statement.expression.left);
|
||||
}
|
||||
|
||||
export function isAssignment(expression: ts.Expression):
|
||||
expression is ts.AssignmentExpression<ts.EqualsToken> {
|
||||
return ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
export function isAssignment(node: ts.Node): node is ts.AssignmentExpression<ts.EqualsToken> {
|
||||
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1264,6 +1426,38 @@ function getCalleeName(call: ts.CallExpression): string|null {
|
||||
|
||||
///////////// Internal Helpers /////////////
|
||||
|
||||
/**
|
||||
* In ES2015, a class may be declared using a variable declaration of the following structure:
|
||||
*
|
||||
* ```
|
||||
* var MyClass = MyClass_1 = class MyClass {};
|
||||
* ```
|
||||
*
|
||||
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
|
||||
* `class MyClass {}` expression is returned as declaration of `MyClass`. Note that if `node`
|
||||
* represents a regular class declaration, it will be returned as-is.
|
||||
*
|
||||
* @param node the node that represents the class whose declaration we are finding.
|
||||
* @returns the declaration of the class or `null` if it is not a "class".
|
||||
*/
|
||||
function getInnerClassDeclaration(node: ts.Node):
|
||||
ClassDeclaration<ts.ClassDeclaration|ts.ClassExpression>|null {
|
||||
// Recognize a variable declaration of the form `var MyClass = class MyClass {}` or
|
||||
// `var MyClass = MyClass_1 = class MyClass {};`
|
||||
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
|
||||
node = node.initializer;
|
||||
while (isAssignment(node)) {
|
||||
node = node.right;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return hasNameIdentifier(node) ? node : null;
|
||||
}
|
||||
|
||||
function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] {
|
||||
// The arguments of a decorator are held in the `args` property of its declaration object.
|
||||
const argsProperty = node.properties.filter(ts.isPropertyAssignment)
|
||||
@ -1333,6 +1527,31 @@ function collectExportedDeclarations(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to resolve the variable declaration that the given declaration is assigned to.
|
||||
* For example, for the following code:
|
||||
*
|
||||
* ```
|
||||
* var MyClass = MyClass_1 = class MyClass {};
|
||||
* ```
|
||||
*
|
||||
* and the provided declaration being `class MyClass {}`, this will return the `var MyClass`
|
||||
* declaration.
|
||||
*
|
||||
* @param declaration The declaration for which any variable declaration should be obtained.
|
||||
* @returns the outer variable declaration if found, undefined otherwise.
|
||||
*/
|
||||
function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.VariableDeclaration|
|
||||
undefined {
|
||||
let node = declaration.parent;
|
||||
|
||||
// Detect an intermediary variable assignment and skip over it.
|
||||
if (isAssignment(node) && ts.isIdentifier(node.left)) {
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return ts.isVariableDeclaration(node) ? node : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
|
@ -9,6 +9,7 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {isFromDtsFile} from '../../../src/ngtsc/util/src/typescript';
|
||||
import {getNameText, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
|
||||
@ -33,11 +34,6 @@ import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignme
|
||||
*
|
||||
*/
|
||||
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
/**
|
||||
* Check whether the given node actually represents a class.
|
||||
*/
|
||||
isClass(node: ts.Node): node is ClassDeclaration { return !!this.getClassDeclaration(node); }
|
||||
|
||||
/**
|
||||
* Determines whether the given declaration, which should be a "class", has a base "class".
|
||||
*
|
||||
@ -180,7 +176,10 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
* @throws if `declaration` does not resolve to a class declaration.
|
||||
*/
|
||||
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
|
||||
if (super.isClass(clazz)) return super.getMembersOfClass(clazz);
|
||||
// Do not follow ES5's resolution logic when the node resides in a .d.ts file.
|
||||
if (isFromDtsFile(clazz)) {
|
||||
return super.getMembersOfClass(clazz);
|
||||
}
|
||||
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol = this.getInnerFunctionSymbolFromClassDeclaration(clazz);
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {ClassSymbol, ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassSymbol, Declaration, ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {DecoratedClass} from './decorated_class';
|
||||
|
||||
export const PRE_R3_MARKER = '__PRE_R3__';
|
||||
@ -37,9 +37,10 @@ export interface ModuleWithProvidersFunction {
|
||||
*/
|
||||
container: ts.Declaration|null;
|
||||
/**
|
||||
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
|
||||
* The declaration of the class that the `ngModule` property on the `ModuleWithProviders` object
|
||||
* refers to.
|
||||
*/
|
||||
ngModule: ts.Identifier;
|
||||
ngModule: Declaration<ClassDeclaration>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user