diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index 1f8550fdf1..119a4fe5cd 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -80,7 +80,7 @@ export class ModuleWithProvidersAnalyzer { let dtsFn: ts.Declaration|null = null; const containerClass = fn.container && this.host.getClassSymbol(fn.container); if (containerClass) { - const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration); + const dtsClass = this.host.getDtsDeclaration(containerClass.declaration.valueDeclaration); // Get the declaration of the matching static method dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ? dtsClass.members diff --git a/packages/compiler-cli/ngcc/src/analysis/util.ts b/packages/compiler-cli/ngcc/src/analysis/util.ts index 2e2a540ae9..b7837d9f90 100644 --- a/packages/compiler-cli/ngcc/src/analysis/util.ts +++ b/packages/compiler-cli/ngcc/src/analysis/util.ts @@ -20,9 +20,9 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.Sour } export function analyzeDecorators( - symbol: NgccClassSymbol, decorators: Decorator[] | null, + classSymbol: NgccClassSymbol, decorators: Decorator[] | null, handlers: DecoratorHandler[]): AnalyzedClass|null { - const declaration = symbol.valueDeclaration; + const declaration = classSymbol.declaration.valueDeclaration; const matchingHandlers = handlers .map(handler => { const detected = handler.detect(declaration, decorators); @@ -78,7 +78,7 @@ export function analyzeDecorators( } } return { - name: symbol.name, + name: classSymbol.name, declaration, decorators, matches, diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts index e6b9ae83e6..f752412df3 100644 --- a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -64,7 +64,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { if (esm5HelperCalls.length > 0) { return esm5HelperCalls; } else { - const sourceFile = classSymbol.valueDeclaration.getSourceFile(); + const sourceFile = classSymbol.declaration.valueDeclaration.getSourceFile(); return this.getTopLevelHelperCalls(sourceFile, helperName); } } diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 927409aa8f..150f9b1716 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -15,7 +15,7 @@ import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils'; -import {ModuleWithProvidersFunction, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; +import {ClassSymbol, ModuleWithProvidersFunction, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; export const DECORATORS = 'decorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String; @@ -91,7 +91,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } /** - * Find the declaration of a node that we think is a class. + * Find a symbol for a node that we think is a class. * Classes should have a `name` identifier, because they may need to be referenced in other parts * of the program. * @@ -104,22 +104,111 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * 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 { - return getInnerClassDeclaration(node) || undefined; - } - - /** - * Find a symbol for a node that we think is a class. - * @param node the node whose symbol we are finding. + * @param declaration the declaration node whose symbol we are finding. * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. */ getClassSymbol(declaration: ts.Node): NgccClassSymbol|undefined { - const classDeclaration = this.getClassDeclaration(declaration); - return classDeclaration && - this.checker.getSymbolAtLocation(classDeclaration.name) as NgccClassSymbol; + const symbol = this.getClassSymbolFromOuterDeclaration(declaration); + if (symbol !== undefined) { + return symbol; + } + + return this.getClassSymbolFromInnerDeclaration(declaration); + } + + /** + * In ES2015, a class may be declared using a variable declaration of the following structure: + * + * ``` + * var MyClass = MyClass_1 = class MyClass {}; + * ``` + * + * This method extracts the `NgccClassSymbol` for `MyClass` when provided with the `var MyClass` + * declaration node. When the `class MyClass {}` node or any other node is given, this method will + * return undefined instead. + * + * @param declaration the declaration whose symbol we are finding. + * @returns the symbol for the node or `undefined` if it does not represent an outer declaration + * of a class. + */ + protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + // Create a symbol without inner declaration if the declaration is a regular class declaration. + if (ts.isClassDeclaration(declaration) && hasNameIdentifier(declaration)) { + return this.createClassSymbol(declaration, null); + } + + // Otherwise, the declaration may be a variable declaration, in which case it must be + // initialized using a class expression as inner declaration. + if (ts.isVariableDeclaration(declaration) && hasNameIdentifier(declaration)) { + const innerDeclaration = getInnerClassDeclaration(declaration); + if (innerDeclaration !== null) { + return this.createClassSymbol(declaration, innerDeclaration); + } + } + + return undefined; + } + + /** + * In ES2015, a class may be declared using a variable declaration of the following structure: + * + * ``` + * var MyClass = MyClass_1 = class MyClass {}; + * ``` + * + * This method extracts the `NgccClassSymbol` for `MyClass` when provided with the + * `class MyClass {}` declaration node. When the `var MyClass` node or any other node is given, + * this method will return undefined instead. + * + * @param declaration the declaration whose symbol we are finding. + * @returns the symbol for the node or `undefined` if it does not represent an inner declaration + * of a class. + */ + protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + if (!ts.isClassExpression(declaration) || !hasNameIdentifier(declaration)) { + return undefined; + } + + const outerDeclaration = getVariableDeclarationOfDeclaration(declaration); + if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) { + return undefined; + } + + return this.createClassSymbol(outerDeclaration, declaration); + } + + /** + * Creates an `NgccClassSymbol` from an outer and inner declaration. If a class only has an outer + * declaration, the "implementation" symbol of the created `NgccClassSymbol` will be set equal to + * the "declaration" symbol. + * + * @param outerDeclaration The outer declaration node of the class. + * @param innerDeclaration The inner declaration node of the class, or undefined if no inner + * declaration is present. + * @returns the `NgccClassSymbol` representing the class, or undefined if a `ts.Symbol` for any of + * the declarations could not be resolved. + */ + protected createClassSymbol( + outerDeclaration: ClassDeclaration, innerDeclaration: ClassDeclaration|null): NgccClassSymbol + |undefined { + const declarationSymbol = + this.checker.getSymbolAtLocation(outerDeclaration.name) as ClassSymbol | undefined; + if (declarationSymbol === undefined) { + return undefined; + } + + const implementationSymbol = innerDeclaration !== null ? + this.checker.getSymbolAtLocation(innerDeclaration.name) : + declarationSymbol; + if (implementationSymbol === undefined) { + return undefined; + } + + return { + name: declarationSymbol.name, + declaration: declarationSymbol, + implementation: implementationSymbol, + }; } /** @@ -221,7 +310,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * Check whether the given node actually represents a class. */ isClass(node: ts.Node): node is ClassDeclaration { - return super.isClass(node) || !!this.getClassDeclaration(node); + return super.isClass(node) || this.getClassSymbol(node) !== undefined; } /** @@ -549,7 +638,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol |undefined { - return symbol.exports && symbol.exports.get(propertyName); + return symbol.implementation.exports && symbol.implementation.exports.get(propertyName); } /** @@ -561,8 +650,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * @returns all information of the decorators on the class. */ protected acquireDecoratorInfo(classSymbol: NgccClassSymbol): DecoratorInfo { - if (this.decoratorCache.has(classSymbol.valueDeclaration)) { - return this.decoratorCache.get(classSymbol.valueDeclaration) !; + const decl = classSymbol.declaration.valueDeclaration; + if (this.decoratorCache.has(decl)) { + return this.decoratorCache.get(decl) !; } // First attempt extracting decorators from static properties. @@ -572,7 +662,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N decoratorInfo = this.computeDecoratorInfoFromHelperCalls(classSymbol); } - this.decoratorCache.set(classSymbol.valueDeclaration, decoratorInfo); + this.decoratorCache.set(decl, decoratorInfo); return decoratorInfo; } @@ -665,8 +755,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N // The member map contains all the method (instance and static); and any instance properties // that are initialized in the class. - if (symbol.members) { - symbol.members.forEach((value, key) => { + if (symbol.implementation.members) { + symbol.implementation.members.forEach((value, key) => { const decorators = decoratorsMap.get(key as string); const reflectedMembers = this.reflectMembers(value, decorators); if (reflectedMembers) { @@ -677,8 +767,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } // The static property map contains all the static properties - if (symbol.exports) { - symbol.exports.forEach((value, key) => { + if (symbol.implementation.exports) { + symbol.implementation.exports.forEach((value, key) => { const decorators = decoratorsMap.get(key as string); const reflectedMembers = this.reflectMembers(value, decorators, true); if (reflectedMembers) { @@ -697,11 +787,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N // } // MyClass.staticProperty = ...; // ``` - const variableDeclaration = getVariableDeclarationOfDeclaration(symbol.valueDeclaration); - if (variableDeclaration !== undefined) { - const variableSymbol = this.checker.getSymbolAtLocation(variableDeclaration.name); - if (variableSymbol && variableSymbol.exports) { - variableSymbol.exports.forEach((value, key) => { + if (ts.isVariableDeclaration(symbol.declaration.valueDeclaration)) { + if (symbol.declaration.exports) { + symbol.declaration.exports.forEach((value, key) => { const decorators = decoratorsMap.get(key as string); const reflectedMembers = this.reflectMembers(value, decorators, true); if (reflectedMembers) { @@ -1182,8 +1270,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol): ts.ParameterDeclaration[]|null { - if (classSymbol.members && classSymbol.members.has(CONSTRUCTOR)) { - const constructorSymbol = classSymbol.members.get(CONSTRUCTOR) !; + const members = classSymbol.implementation.members; + if (members && members.has(CONSTRUCTOR)) { + const constructorSymbol = members.get(CONSTRUCTOR) !; // For some reason the constructor does not have a `valueDeclaration` ?!? const constructor = constructorSymbol.declarations && constructorSymbol.declarations[0] as ts.ConstructorDeclaration | undefined; @@ -1314,7 +1403,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * @returns an array of statements that may contain helper calls. */ protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] { - return Array.from(classSymbol.valueDeclaration.getSourceFile().statements); + return Array.from(classSymbol.declaration.valueDeclaration.getSourceFile().statements); } /** @@ -1637,28 +1726,29 @@ function getCalleeName(call: ts.CallExpression): string|null { * ``` * * 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. + * `class MyClass {}` expression is returned as declaration of `var MyClass`. If the variable + * is not initialized using a class expression, null is returned. * * @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|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)) { +function getInnerClassDeclaration(node: ts.Node): ClassDeclaration|null { + if (!ts.isVariableDeclaration(node) || node.initializer === undefined) { return null; } - return hasNameIdentifier(node) ? node : null; + // Recognize a variable declaration of the form `var MyClass = class MyClass {}` or + // `var MyClass = MyClass_1 = class MyClass {};` + let expression = node.initializer; + while (isAssignment(expression)) { + expression = expression.right; + } + + if (!ts.isClassExpression(expression) || !hasNameIdentifier(expression)) { + return null; + } + + return expression; } function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 5b5908d92f..59a177a1a8 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -8,8 +8,7 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; -import {isFromDtsFile} from '../../../src/ngtsc/util/src/typescript'; +import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; @@ -45,10 +44,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { hasBaseClass(clazz: ClassDeclaration): boolean { if (super.hasBaseClass(clazz)) return true; - const classDeclaration = this.getClassDeclaration(clazz); - if (!classDeclaration) return false; + const classSymbol = this.getClassSymbol(clazz); + if (classSymbol === undefined) { + return false; + } - const iifeBody = getIifeBody(classDeclaration); + const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration); if (!iifeBody) return false; const iife = iifeBody.parent; @@ -63,10 +64,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return superBaseClassIdentifier; } - const classDeclaration = this.getClassDeclaration(clazz); - if (!classDeclaration) return null; + const classSymbol = this.getClassSymbol(clazz); + if (classSymbol === undefined) { + return null; + } - const iifeBody = getIifeBody(classDeclaration); + const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration); if (!iifeBody) return null; const iife = iifeBody.parent; @@ -84,39 +87,61 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { } /** - * Find the declaration of a class given a node that we think represents the class. - * * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE, * whose value is assigned to a variable (which represents the class to the rest of the program). * So we might need to dig around to get hold of the "class" declaration. * - * `node` might be one of: - * - A class declaration (from a typings file). - * - The declaration of the outer variable, which is assigned the result of the IIFE. - * - The function declaration inside the IIFE, which is eventually returned and assigned to the - * outer variable. + * This method extracts a `NgccClassSymbol` if `declaration` is the outer variable which is + * assigned the result of the IIFE. Otherwise, undefined is returned. * - * The returned declaration is either the class declaration (from the typings file) or the outer - * variable declaration. - * - * @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". + * @param declaration the declaration whose symbol we are finding. + * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. */ - getClassDeclaration(node: ts.Node): ClassDeclaration|undefined { - const superDeclaration = super.getClassDeclaration(node); - if (superDeclaration) return superDeclaration; + protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + const classSymbol = super.getClassSymbolFromOuterDeclaration(declaration); + if (classSymbol !== undefined) { + return classSymbol; + } - const outerClass = getClassDeclarationFromInnerFunctionDeclaration(node); - if (outerClass) return outerClass; - - // At this point, `node` could be the outer variable declaration of an ES5 class. - // If so, ensure that it has a `name` identifier and the correct structure. - if (!isNamedVariableDeclaration(node) || - !this.getInnerFunctionDeclarationFromClassDeclaration(node)) { + if (!isNamedVariableDeclaration(declaration)) { return undefined; } - return node; + const innerDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(declaration); + if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) { + return undefined; + } + + return this.createClassSymbol(declaration, innerDeclaration); + } + + /** + * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE, + * whose value is assigned to a variable (which represents the class to the rest of the program). + * So we might need to dig around to get hold of the "class" declaration. + * + * This method extracts a `NgccClassSymbol` if `declaration` is the function declaration inside + * the IIFE. Otherwise, undefined is returned. + * + * @param declaration the declaration whose symbol we are finding. + * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. + */ + protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration); + if (classSymbol !== undefined) { + return classSymbol; + } + + if (!ts.isFunctionDeclaration(declaration) || !hasNameIdentifier(declaration)) { + return undefined; + } + + const outerDeclaration = getClassDeclarationFromInnerFunctionDeclaration(declaration); + if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) { + return undefined; + } + + return this.createClassSymbol(outerDeclaration, declaration); } /** @@ -211,43 +236,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return {node, body: statements || null, helper: null, parameters}; } - /** - * Examine a declaration which should be of a class, and return metadata about the members of the - * class. - * - * @param declaration a TypeScript `ts.Declaration` node representing the class over which to - * reflect. - * - * @returns an array of `ClassMember` metadata representing the members of the class. - * - * @throws if `declaration` does not resolve to a class declaration. - */ - getMembersOfClass(clazz: ClassDeclaration): ClassMember[] { - // 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); - if (!innerFunctionSymbol) { - throw new Error( - `Attempted to get members of a non-class: "${(clazz as ClassDeclaration).getText()}"`); - } - - return this.getMembersOfSymbol(innerFunctionSymbol); - } - - /** Gets all decorators of the given class symbol. */ - getDecoratorsOfSymbol(symbol: NgccClassSymbol): Decorator[]|null { - // The necessary info is on the inner function declaration (inside the ES5 class IIFE). - const innerFunctionSymbol = - this.getInnerFunctionSymbolFromClassDeclaration(symbol.valueDeclaration); - if (!innerFunctionSymbol) return null; - - return super.getDecoratorsOfSymbol(innerFunctionSymbol); - } - ///////////// Protected Helpers ///////////// @@ -288,29 +276,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return functionDeclaration; } - /** - * Get the identifier symbol of the inner function declaration of an ES5-style class. - * - * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE - * and returned to be assigned to a variable outside the IIFE, which is what the rest of the - * program interacts with. - * - * Given the outer variable declaration, we want to get to the identifier symbol of the inner - * function declaration. - * - * @param clazz a node that could be the variable expression outside an ES5 class IIFE. - * @param checker the TS program TypeChecker - * @returns the inner function declaration identifier symbol or `undefined` if it is not a "class" - * or has no identifier. - */ - protected getInnerFunctionSymbolFromClassDeclaration(clazz: ClassDeclaration): NgccClassSymbol - |undefined { - const innerFunctionDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(clazz); - if (!innerFunctionDeclaration || !hasNameIdentifier(innerFunctionDeclaration)) return undefined; - - return this.checker.getSymbolAtLocation(innerFunctionDeclaration.name) as NgccClassSymbol; - } - /** * Find the declarations of the constructor parameters of a class identified by its symbol. * @@ -325,9 +290,8 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { */ protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol): ts.ParameterDeclaration[]|null { - const constructor = - this.getInnerFunctionDeclarationFromClassDeclaration(classSymbol.valueDeclaration); - if (!constructor) return null; + const constructor = classSymbol.implementation.valueDeclaration; + if (!ts.isFunctionDeclaration(constructor)) return null; if (constructor.parameters.length > 0) { return Array.from(constructor.parameters); @@ -340,24 +304,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return []; } - /** - * Get the parameter decorators of a class constructor. - * - * @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose - * parameter info we want to get. - * @param parameterNodes the array of TypeScript parameter nodes for this class's constructor. - * @returns an array of constructor parameter info objects. - */ - protected getConstructorParamInfo( - classSymbol: NgccClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] { - // The necessary info is on the inner function declaration (inside the ES5 class IIFE). - const innerFunctionSymbol = - this.getInnerFunctionSymbolFromClassDeclaration(classSymbol.valueDeclaration); - if (!innerFunctionSymbol) return []; - - return super.getConstructorParamInfo(innerFunctionSymbol, parameterNodes); - } - /** * Get the parameter type and decorators for the constructor of a class, * where the information is stored on a static method of the class. @@ -481,7 +427,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { * @returns an array of statements that may contain helper calls. */ protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] { - const classDeclarationParent = classSymbol.valueDeclaration.parent; + const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent; return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : []; } @@ -499,26 +445,14 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { */ protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol |undefined { - // The symbol corresponds with the inner function declaration. First lets see if the static - // property is set there. - const prop = super.getStaticProperty(symbol, propertyName); + // First lets see if the static property can be resolved from the inner class symbol. + const prop = symbol.implementation.exports && symbol.implementation.exports.get(propertyName); if (prop !== undefined) { return prop; } - // Otherwise, obtain the outer variable declaration and resolve its symbol, in order to lookup - // static properties there. - const outerClass = getClassDeclarationFromInnerFunctionDeclaration(symbol.valueDeclaration); - if (outerClass === undefined) { - return undefined; - } - - const outerSymbol = this.checker.getSymbolAtLocation(outerClass.name); - if (outerSymbol === undefined || outerSymbol.valueDeclaration === undefined) { - return undefined; - } - - return super.getStaticProperty(outerSymbol as NgccClassSymbol, propertyName); + // Otherwise, lookup the static properties on the outer class symbol. + return symbol.declaration.exports && symbol.declaration.exports.get(propertyName); } } diff --git a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts index 6fa2e9e248..a52f641816 100644 --- a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts @@ -47,7 +47,32 @@ export interface ModuleWithProvidersFunction { * The symbol corresponding to a "class" declaration. I.e. a `ts.Symbol` whose `valueDeclaration` is * a `ClassDeclaration`. */ -export type NgccClassSymbol = ts.Symbol & {valueDeclaration: ClassDeclaration}; +export type ClassSymbol = ts.Symbol & {valueDeclaration: ClassDeclaration}; + +/** + * A representation of a class that accounts for the potential existence of two `ClassSymbol`s for a + * given class, as the compiled JavaScript bundles that ngcc reflects on can have two declarations. + */ +export interface NgccClassSymbol { + /** + * The name of the class. + */ + name: string; + + /** + * Represents the symbol corresponding with the outer declaration of the class. This should be + * considered the public class symbol, i.e. its declaration is visible to the rest of the program. + */ + declaration: ClassSymbol; + + /** + * Represents the symbol corresponding with the inner declaration of the class, referred to as its + * "implementation". This is not necessarily a `ClassSymbol` but rather just a `ts.Symbol`, as the + * inner declaration does not need to satisfy the requirements imposed on a publicly visible class + * declaration. + */ + implementation: ts.Symbol; +} /** * A reflection host that has extra methods for looking at non-Typescript package formats @@ -59,7 +84,7 @@ export interface NgccReflectionHost extends ReflectionHost { * @returns the symbol for the declaration or `undefined` if it is not * a "class" or has no symbol. */ - getClassSymbol(node: ts.Node): NgccClassSymbol|undefined; + getClassSymbol(declaration: ts.Node): NgccClassSymbol|undefined; /** * Search the given module for variable declarations in which the initializer diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts index 901d28a5d7..8a2e4786d6 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -79,7 +79,7 @@ export class EsmRenderingFormatter implements RenderingFormatter { if (!classSymbol) { throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); } - const insertionPoint = classSymbol.valueDeclaration !.getEnd(); + const insertionPoint = classSymbol.declaration.valueDeclaration !.getEnd(); output.appendLeft(insertionPoint, '\n' + definitions); } diff --git a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts index 41e1ec337c..f4f8981f1d 100644 --- a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts @@ -15,8 +15,14 @@ import {NgccClassSymbol} from '../../src/host/ngcc_host'; describe('DefaultMigrationHost', () => { describe('injectSyntheticDecorator()', () => { const mockHost: any = { - getClassSymbol: (node: any): NgccClassSymbol | undefined => - ({ valueDeclaration: node, name: node.name.text } as any), + getClassSymbol: (node: any): NgccClassSymbol | undefined => { + const symbol = { valueDeclaration: node, name: node.name.text } as any; + return { + name: node.name.text, + declaration: symbol, + implementation: symbol, + }; + }, }; const mockMetadata: any = {}; const mockEvaluator: any = {}; diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index c147a7d192..a688fcf630 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -13,7 +13,6 @@ import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassD import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; -import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {getIifeBody} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; import {getRootFiles, makeTestBundleProgram, makeTestDtsBundleProgram} from '../helpers/utils'; @@ -1712,19 +1711,22 @@ exports.ExternalModule = ExternalModule; const classSymbol = host.getClassSymbol(node); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(node); + expect(classSymbol !.implementation.valueDeclaration).toBe(node); }); it('should return the class symbol for an ES5 class (outer variable declaration)', () => { loadTestFiles([SIMPLE_CLASS_FILE]); const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - const node = getDeclaration( + const outerNode = getDeclaration( program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const classSymbol = host.getClassSymbol(node); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the class symbol for an ES5 class (inner function declaration)', () => { @@ -1737,7 +1739,8 @@ exports.ExternalModule = ExternalModule; const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(outerNode); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the same class symbol (of the outer declaration) for outer and inner declarations', @@ -1751,7 +1754,10 @@ exports.ExternalModule = ExternalModule; const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + const innerSymbol = host.getClassSymbol(innerNode) !; + const outerSymbol = host.getClassSymbol(outerNode) !; + expect(innerSymbol.declaration).toBe(outerSymbol.declaration); + expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); it('should return undefined if node is not an ES5 class', () => { @@ -1764,46 +1770,67 @@ exports.ExternalModule = ExternalModule; expect(classSymbol).toBeUndefined(); }); + + it('should return undefined if variable declaration is not initialized using an IIFE', + () => { + const testFile = { + name: _('/test.js'), + contents: `var MyClass = null;`, + }; + loadTestFiles([testFile]); + const {program, host: compilerHost} = makeTestBundleProgram(testFile.name); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); }); describe('isClass()', () => { - let host: CommonJsReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; - - beforeEach(() => { - loadTestFiles([SIMPLE_CLASS_FILE]); - const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); - host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); - mockNode = {} as any; - - getClassDeclarationSpy = spyOn(CommonJsReflectionHost.prototype, 'getClassDeclaration'); - superGetClassDeclarationSpy = - spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + it('should return true if a given node is a TS class declaration', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + expect(host.isClass(node)).toBe(true); }); - it('should return true if superclass returns true', () => { - superGetClassDeclarationSpy.and.returnValue(true); - getClassDeclarationSpy.and.callThrough(); + it('should return true if a given node is the outer variable declaration of a class', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); + }); - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); + it('should return true if a given node is the inner variable declaration of a class', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + expect(host.isClass(innerNode)).toBe(true); + }); - it('should return true if it can find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(true); - - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); - - it('should return false if it cannot find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(false); - - expect(host.isClass(mockNode)).toBe(false); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + it('should return false if a given node is a function declaration', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(host.isClass(node)).toBe(false); }); }); diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index 0564197ed3..5db8a3fa05 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -1565,6 +1565,95 @@ runInEachFileSystem(() => { }); }); + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(node); + expect(classSymbol !.implementation.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for a class expression (outer variable declaration)', + () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = (outerNode.initializer as ts.ClassExpression); + const classSymbol = host.getClassSymbol(outerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + + it('should return the class symbol for a class expression (inner class expression)', () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = (outerNode.initializer as ts.ClassExpression); + const classSymbol = host.getClassSymbol(innerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); + }); + + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', + () => { + loadTestFiles([CLASS_EXPRESSION_FILE]); + const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = getDeclaration( + program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = (outerNode.initializer as ts.ClassExpression); + + const innerSymbol = host.getClassSymbol(innerNode) !; + const outerSymbol = host.getClassSymbol(outerNode) !; + expect(innerSymbol.declaration).toBe(outerSymbol.declaration); + expect(innerSymbol.implementation).toBe(outerSymbol.implementation); + }); + + it('should return undefined if node is not a class', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); + + it('should return undefined if variable declaration is not initialized using a class expression', + () => { + const testFile = { + name: _('/test.js'), + contents: `var MyClass = null;`, + }; + loadTestFiles([testFile]); + const {program} = makeTestBundleProgram(testFile.name); + const host = + new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); + }); + describe('isClass()', () => { it('should return true if a given node is a TS class declaration', () => { loadTestFiles([SIMPLE_CLASS_FILE]); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 2745d649c6..40abe40388 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -1874,19 +1874,22 @@ runInEachFileSystem(() => { const classSymbol = host.getClassSymbol(node); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(node); + expect(classSymbol !.implementation.valueDeclaration).toBe(node); }); it('should return the class symbol for an ES5 class (outer variable declaration)', () => { loadTestFiles([SIMPLE_CLASS_FILE]); const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); - const node = getDeclaration( + const outerNode = getDeclaration( program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const classSymbol = host.getClassSymbol(node); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the class symbol for an ES5 class (inner function declaration)', () => { @@ -1899,7 +1902,8 @@ runInEachFileSystem(() => { const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(outerNode); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the same class symbol (of the outer declaration) for outer and inner declarations', @@ -1911,7 +1915,10 @@ runInEachFileSystem(() => { program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + const innerSymbol = host.getClassSymbol(innerNode) !; + const outerSymbol = host.getClassSymbol(outerNode) !; + expect(innerSymbol.declaration).toBe(outerSymbol.declaration); + expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); it('should return undefined if node is not an ES5 class', () => { @@ -1924,43 +1931,58 @@ runInEachFileSystem(() => { expect(classSymbol).toBeUndefined(); }); + + it('should return undefined if variable declaration is not initialized using an IIFE', () => { + const testFile = { + name: _('/test.js'), + contents: `var MyClass = null;`, + }; + loadTestFiles([testFile]); + const {program} = makeTestBundleProgram(testFile.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); }); describe('isClass()', () => { - let host: Esm5ReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; - - beforeEach(() => { - host = new Esm5ReflectionHost(new MockLogger(), false, null as any); - mockNode = {} as any; - - getClassDeclarationSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassDeclaration'); - superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + it('should return true if a given node is a TS class declaration', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + expect(host.isClass(node)).toBe(true); }); - it('should return true if superclass returns true', () => { - superGetClassDeclarationSpy.and.returnValue(true); - getClassDeclarationSpy.and.callThrough(); - - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + it('should return true if a given node is the outer variable declaration of a class', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); }); - it('should return true if it can find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(true); - - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + it('should return true if a given node is the inner variable declaration of a class', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + expect(host.isClass(innerNode)).toBe(true); }); - it('should return false if it cannot find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(false); - - expect(host.isClass(mockNode)).toBe(false); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + it('should return false if a given node is a function declaration', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(host.isClass(node)).toBe(false); }); }); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index f0e69157b4..74566a1bee 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -1783,19 +1783,22 @@ runInEachFileSystem(() => { const classSymbol = host.getClassSymbol(node); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(node); + expect(classSymbol !.implementation.valueDeclaration).toBe(node); }); it('should return the class symbol for an ES5 class (outer variable declaration)', () => { loadTestFiles([SIMPLE_CLASS_FILE]); const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - const node = getDeclaration( + const outerNode = getDeclaration( program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const classSymbol = host.getClassSymbol(node); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(node); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the class symbol for an ES5 class (inner function declaration)', () => { @@ -1808,7 +1811,8 @@ runInEachFileSystem(() => { const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); - expect(classSymbol !.valueDeclaration).toBe(outerNode); + expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode); }); it('should return the same class symbol (of the outer declaration) for outer and inner declarations', @@ -1821,7 +1825,10 @@ runInEachFileSystem(() => { const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + const innerSymbol = host.getClassSymbol(innerNode) !; + const outerSymbol = host.getClassSymbol(outerNode) !; + expect(innerSymbol.declaration).toBe(outerSymbol.declaration); + expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); it('should return undefined if node is not an ES5 class', () => { @@ -1834,46 +1841,64 @@ runInEachFileSystem(() => { expect(classSymbol).toBeUndefined(); }); + + it('should return undefined if variable declaration is not initialized using an IIFE', + () => { + const testFile = { + name: _('/test.js'), + contents: `var MyClass = null;`, + }; + loadTestFiles([testFile]); + const {program, host: compilerHost} = makeTestBundleProgram(testFile.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); }); describe('isClass()', () => { - let host: UmdReflectionHost; - let mockNode: ts.Node; - let getClassDeclarationSpy: jasmine.Spy; - let superGetClassDeclarationSpy: jasmine.Spy; - - beforeEach(() => { - loadTestFiles([SIMPLE_CLASS_FILE]); - const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); - host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); - mockNode = {} as any; - - getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration'); - superGetClassDeclarationSpy = - spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + it('should return true if a given node is a TS class declaration', () => { + loadTestFiles([SIMPLE_ES2015_CLASS_FILE]); + const {program, host: compilerHost} = + makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + expect(host.isClass(node)).toBe(true); }); - it('should return true if superclass returns true', () => { - superGetClassDeclarationSpy.and.returnValue(true); - getClassDeclarationSpy.and.callThrough(); + it('should return true if a given node is the outer variable declaration of a class', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); + }); - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); + it('should return true if a given node is the inner variable declaration of a class', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = + getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + expect(host.isClass(innerNode)).toBe(true); + }); - it('should return true if it can find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(true); - - expect(host.isClass(mockNode)).toBe(true); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); - }); - - it('should return false if it cannot find a declaration for the class', () => { - getClassDeclarationSpy.and.returnValue(false); - - expect(host.isClass(mockNode)).toBe(false); - expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + it('should return false if a given node is a function declaration', () => { + loadTestFiles([FOO_FUNCTION_FILE]); + const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(host.isClass(node)).toBe(false); }); });