diff --git a/packages/compiler-cli/ngcc/src/host/decorated_class.ts b/packages/compiler-cli/ngcc/src/host/decorated_class.ts index addfa5d3d2..15c570ade2 100644 --- a/packages/compiler-cli/ngcc/src/host/decorated_class.ts +++ b/packages/compiler-cli/ngcc/src/host/decorated_class.ts @@ -17,7 +17,10 @@ export class DecoratedClass { * Initialize a `DecoratedClass` that was found in a `DecoratedFile`. * @param name The name of the class that has been found. This is mostly used * for informational purposes. - * @param declaration The TypeScript AST node where this class is declared + * @param declaration The TypeScript AST node where this class is declared. In ES5 code, where a + * class can be represented by both a variable declaration and a function declaration (inside an + * IIFE), `declaration` will always refer to the outer variable declaration, which represents the + * class to the rest of the program. * @param decorators The collection of decorators that have been found on this class. */ constructor( diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index d412e22a59..b370b7347d 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {BundleProgram} from '../packages/bundle_program'; -import {findAll, getNameText, isDefined} from '../utils'; +import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils'; import {DecoratedClass} from './decorated_class'; import {ModuleWithProvidersFunction, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; @@ -54,6 +54,37 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N this.dtsDeclarationMap = dts && this.computeDtsDeclarationMap(dts.path, dts.program) || null; } + /** + * Find the declaration of 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. + * + * @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; + } + + /** + * Find a symbol for a node that we think is a class. + * @param node the 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): ClassSymbol|undefined { + const classDeclaration = this.getClassDeclaration(declaration); + return classDeclaration && + this.checker.getSymbolAtLocation(classDeclaration.name) as ClassSymbol; + } + /** * Examine a declaration (for example, of a class or function) and return metadata about any * decorators present on the declaration. @@ -86,79 +117,12 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * @throws if `declaration` does not resolve to a class declaration. */ getMembersOfClass(clazz: ClassDeclaration): ClassMember[] { - const members: ClassMember[] = []; - const symbol = this.getClassSymbol(clazz); - if (!symbol) { + const classSymbol = this.getClassSymbol(clazz); + if (!classSymbol) { throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`); } - // The decorators map contains all the properties that are decorated - const decoratorsMap = this.getMemberDecorators(symbol); - - // 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) => { - const decorators = decoratorsMap.get(key as string); - const reflectedMembers = this.reflectMembers(value, decorators); - if (reflectedMembers) { - decoratorsMap.delete(key as string); - members.push(...reflectedMembers); - } - }); - } - - // The static property map contains all the static properties - if (symbol.exports) { - symbol.exports.forEach((value, key) => { - const decorators = decoratorsMap.get(key as string); - const reflectedMembers = this.reflectMembers(value, decorators, true); - if (reflectedMembers) { - decoratorsMap.delete(key as string); - members.push(...reflectedMembers); - } - }); - } - - // If this class was declared as a VariableDeclaration then it may have static properties - // attached to the variable rather than the class itself - // For example: - // ``` - // let MyClass = class MyClass { - // // no static properties here! - // } - // MyClass.staticProperty = ...; - // ``` - if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) { - const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name); - if (variableSymbol && variableSymbol.exports) { - variableSymbol.exports.forEach((value, key) => { - const decorators = decoratorsMap.get(key as string); - const reflectedMembers = this.reflectMembers(value, decorators, true); - if (reflectedMembers) { - decoratorsMap.delete(key as string); - members.push(...reflectedMembers); - } - }); - } - } - - // Deal with any decorated properties that were not initialized in the class - decoratorsMap.forEach((value, key) => { - members.push({ - implementation: null, - decorators: value, - isStatic: false, - kind: ClassMemberKind.Property, - name: key, - nameNode: null, - node: null, - type: null, - value: null - }); - }); - - return members; + return this.getMembersOfSymbol(classSymbol); } /** @@ -188,24 +152,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return null; } - /** - * Find a symbol for a node that we think is a class. - * @param node the 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): ClassSymbol|undefined { - if (ts.isClassDeclaration(declaration)) { - return declaration.name && this.checker.getSymbolAtLocation(declaration.name) as ClassSymbol; - } - if (ts.isVariableDeclaration(declaration) && declaration.initializer) { - declaration = declaration.initializer; - } - if (ts.isClassExpression(declaration)) { - return declaration.name && this.checker.getSymbolAtLocation(declaration.name) as ClassSymbol; - } - return undefined; - } - /** * Search the given module for variable declarations in which the initializer * is an identifier marked with the `PRE_R3_MARKER`. @@ -497,6 +443,84 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return decorators.length ? decorators : null; } + /** + * Examine a symbol which should be of a class, and return metadata about its members. + * + * @param symbol the `ClassSymbol` representing the class over which to reflect. + * @returns an array of `ClassMember` metadata representing the members of the class. + */ + protected getMembersOfSymbol(symbol: ClassSymbol): ClassMember[] { + const members: ClassMember[] = []; + + // The decorators map contains all the properties that are decorated + const decoratorsMap = this.getMemberDecorators(symbol); + + // 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) => { + const decorators = decoratorsMap.get(key as string); + const reflectedMembers = this.reflectMembers(value, decorators); + if (reflectedMembers) { + decoratorsMap.delete(key as string); + members.push(...reflectedMembers); + } + }); + } + + // The static property map contains all the static properties + if (symbol.exports) { + symbol.exports.forEach((value, key) => { + const decorators = decoratorsMap.get(key as string); + const reflectedMembers = this.reflectMembers(value, decorators, true); + if (reflectedMembers) { + decoratorsMap.delete(key as string); + members.push(...reflectedMembers); + } + }); + } + + // If this class was declared as a VariableDeclaration then it may have static properties + // attached to the variable rather than the class itself + // For example: + // ``` + // let MyClass = class MyClass { + // // no static properties here! + // } + // MyClass.staticProperty = ...; + // ``` + if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) { + const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name); + if (variableSymbol && variableSymbol.exports) { + variableSymbol.exports.forEach((value, key) => { + const decorators = decoratorsMap.get(key as string); + const reflectedMembers = this.reflectMembers(value, decorators, true); + if (reflectedMembers) { + decoratorsMap.delete(key as string); + members.push(...reflectedMembers); + } + }); + } + } + + // Deal with any decorated properties that were not initialized in the class + decoratorsMap.forEach((value, key) => { + members.push({ + implementation: null, + decorators: value, + isStatic: false, + kind: ClassMemberKind.Property, + name: key, + nameNode: null, + node: null, + type: null, + value: null + }); + }); + + return members; + } + /** * Get all the member decorators for the given class. * @param classSymbol the class whose member decorators we are interested in. diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index ec1a44a104..235f2d81b3 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {getNameText, hasNameIdentifier} from '../utils'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; @@ -36,9 +36,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { /** * Check whether the given node actually represents a class. */ - isClass(node: ts.Node): node is ClassDeclaration { - return super.isClass(node) || !!this.getClassSymbol(node); - } + 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". @@ -48,11 +46,13 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { * @param clazz a `ClassDeclaration` representing the class over which to reflect. */ hasBaseClass(clazz: ClassDeclaration): boolean { - const classSymbol = this.getClassSymbol(clazz); - if (!classSymbol) return false; + if (super.hasBaseClass(clazz)) return true; - const iifeBody = classSymbol.valueDeclaration.parent; - if (!iifeBody || !ts.isBlock(iifeBody)) return false; + const classDeclaration = this.getClassDeclaration(clazz); + if (!classDeclaration) return false; + + const iifeBody = getIifeBody(classDeclaration); + if (!iifeBody) return false; const iife = iifeBody.parent; if (!iife || !ts.isFunctionExpression(iife)) return false; @@ -61,38 +61,39 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { } /** - * Find a symbol for a node that we think is a class. + * 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. - * So we might need to dig around inside to get hold of the "class" symbol. + * 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 declaration file). + * - 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. * - * @param node the top level declaration that represents an exported class or the function - * expression inside the IIFE. - * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. + * 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". */ - getClassSymbol(node: ts.Node): ClassSymbol|undefined { - const symbol = super.getClassSymbol(node); - if (symbol) return symbol; + getClassDeclaration(node: ts.Node): ClassDeclaration|undefined { + const superDeclaration = super.getClassDeclaration(node); + if (superDeclaration) return superDeclaration; - if (ts.isVariableDeclaration(node)) { - const iifeBody = getIifeBody(node); - if (!iifeBody) return undefined; + const outerClass = getClassDeclarationFromInnerFunctionDeclaration(node); + if (outerClass) return outerClass; - const innerClassIdentifier = getReturnIdentifier(iifeBody); - if (!innerClassIdentifier) return undefined; - - return this.checker.getSymbolAtLocation(innerClassIdentifier) as ClassSymbol; + // 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)) { + return undefined; } - const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node); - - return outerClassNode && this.getClassSymbol(outerClassNode); + return node; } /** @@ -115,12 +116,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { // Get the identifier for the outer class node (if any). const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent); - - if (outerClassNode && hasNameIdentifier(outerClassNode)) { - id = outerClassNode.name; - } - - const declaration = super.getDeclarationOfIdentifier(id); + const declaration = super.getDeclarationOfIdentifier(outerClassNode ? outerClassNode.name : id); if (!declaration || !ts.isVariableDeclaration(declaration.node) || declaration.node.initializer !== undefined || @@ -172,31 +168,149 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return {node, body: statements || 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[] { + if (super.isClass(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); + } + ///////////// Protected Helpers ///////////// + /** + * Get 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 inner function declaration. + * + * @param node a node that could be the variable expression outside an ES5 class IIFE. + * @param checker the TS program TypeChecker + * @returns the inner function declaration or `undefined` if it is not a "class". + */ + protected getInnerFunctionDeclarationFromClassDeclaration(node: ts.Node): ts.FunctionDeclaration + |undefined { + if (!ts.isVariableDeclaration(node)) return undefined; + + // Extract the IIFE body (if any). + const iifeBody = getIifeBody(node); + if (!iifeBody) return undefined; + + // Extract the function declaration from inside the IIFE. + const functionDeclaration = iifeBody.statements.find(ts.isFunctionDeclaration); + if (!functionDeclaration) return undefined; + + // Extract the return identifier of the IIFE. + const returnIdentifier = getReturnIdentifier(iifeBody); + const returnIdentifierSymbol = + returnIdentifier && this.checker.getSymbolAtLocation(returnIdentifier); + if (!returnIdentifierSymbol) return undefined; + + // Verify that the inner function is returned. + if (returnIdentifierSymbol.valueDeclaration !== functionDeclaration) return undefined; + + 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): ClassSymbol + |undefined { + const innerFunctionDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(clazz); + if (!innerFunctionDeclaration || !hasNameIdentifier(innerFunctionDeclaration)) return undefined; + + return this.checker.getSymbolAtLocation(innerFunctionDeclaration.name) as ClassSymbol; + } + /** * Find the declarations of the constructor parameters of a class identified by its symbol. * - * In ESM5 there is no "class" so the constructor that we want is actually the declaration - * function itself. + * In ESM5, there is no "class" so the constructor that we want is actually the inner function + * declaration inside the IIFE, whose return value is assigned to the outer variable declaration + * (that represents the class to the rest of the program). * - * @param classSymbol the class whose parameters we want to find. + * @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose + * parameters we want to find. * @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in - * the class's constructor or null if there is no constructor. + * the class's constructor or `null` if there is no constructor. */ protected getConstructorParameterDeclarations(classSymbol: ClassSymbol): ts.ParameterDeclaration[]|null { - const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration; + const constructor = + this.getInnerFunctionDeclarationFromClassDeclaration(classSymbol.valueDeclaration); + if (!constructor) return null; + if (constructor.parameters.length > 0) { return Array.from(constructor.parameters); } + if (isSynthesizedConstructor(constructor)) { return null; } + 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: ClassSymbol, 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); + } + + protected getDecoratorsOfSymbol(symbol: ClassSymbol): 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); + } + /** * Get the parameter type and decorators for the constructor of a class, * where the information is stored on a static method of the class. @@ -389,8 +503,8 @@ function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name * @param node a node that could be the function expression inside an ES5 class IIFE. * @returns the outer variable declaration or `undefined` if it is not a "class". */ -function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.VariableDeclaration| - undefined { +function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): + ClassDeclaration|undefined { if (ts.isFunctionDeclaration(node)) { // It might be the function expression inside the IIFE. We need to go 5 levels up... @@ -414,14 +528,16 @@ function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.Vari outerNode = outerNode.parent; if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined; - return outerNode; + // Finally, ensure that the variable declaration has a `name` identifier. + return hasNameIdentifier(outerNode) ? outerNode : undefined; } return undefined; } -function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined { - if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) { +export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined { + if (!ts.isVariableDeclaration(declaration) || !declaration.initializer || + !ts.isParenthesizedExpression(declaration.initializer)) { return undefined; } const call = declaration.initializer; diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts index c412b0a71b..8c68a68704 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; import MagicString from 'magic-string'; +import {getIifeBody} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; import {CompiledClass} from '../analysis/decoration_analyzer'; import {EsmRenderer} from './esm_renderer'; @@ -23,21 +24,18 @@ export class Esm5Renderer extends EsmRenderer { * Add the definitions to each decorated class */ addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { - const classSymbol = this.host.getClassSymbol(compiledClass.declaration); - if (!classSymbol) { - throw new Error( - `Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); - } - const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent; - if (!parent || !ts.isBlock(parent)) { + const iifeBody = getIifeBody(compiledClass.declaration); + if (!iifeBody) { throw new Error( `Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); } - const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement)); + + const returnStatement = iifeBody.statements.find(ts.isReturnStatement); if (!returnStatement) { throw new Error( `Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); } + const insertionPoint = returnStatement.getFullStart(); output.appendLeft(insertionPoint, '\n' + definitions); } 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 8497412751..4a7ab9581b 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -1536,7 +1536,7 @@ describe('Esm2015ReflectionHost', () => { }); }); - describe('getModuleWithProvidersFunctions', () => { + describe('getModuleWithProvidersFunctions()', () => { it('should find every exported function that returns an object that looks like a ModuleWithProviders object', () => { const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); 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 4063809f5a..369ec31ee7 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -8,9 +8,9 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMemberKind, ClassSymbol, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {Esm5ReflectionHost} from '../../src/host/esm5_host'; +import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; import {expectTypeValueReferencesForParameters} from './util'; @@ -96,6 +96,13 @@ const ACCESSORS_FILE = { `, }; +const SIMPLE_ES2015_CLASS_FILE = { + name: '/simple_es2015_class.d.ts', + contents: ` + export class EmptyClass {} + `, +}; + const SIMPLE_CLASS_FILE = { name: '/simple_class.js', contents: ` @@ -1095,7 +1102,7 @@ describe('Esm5ReflectionHost', () => { }); }); - describe('getConstructorParameters', () => { + describe('getConstructorParameters()', () => { it('should find the decorated constructor parameters', () => { const program = makeTestProgram(SOME_DIRECTIVE_FILE); const host = new Esm5ReflectionHost(false, program.getTypeChecker()); @@ -1408,7 +1415,7 @@ describe('Esm5ReflectionHost', () => { }); }); - describe('getImportOfIdentifier', () => { + describe('getImportOfIdentifier()', () => { it('should find the import of an identifier', () => { const program = makeTestProgram(...IMPORTS_FILES); const host = new Esm5ReflectionHost(false, program.getTypeChecker()); @@ -1440,7 +1447,7 @@ describe('Esm5ReflectionHost', () => { }); }); - describe('getDeclarationOfIdentifier', () => { + describe('getDeclarationOfIdentifier()', () => { it('should return the declaration of a locally defined identifier', () => { const program = makeTestProgram(SOME_DIRECTIVE_FILE); const host = new Esm5ReflectionHost(false, program.getTypeChecker()); @@ -1550,21 +1557,15 @@ describe('Esm5ReflectionHost', () => { }); describe('getClassSymbol()', () => { - let superGetClassSymbolSpy: jasmine.Spy; + it('should return the class symbol for an ES2015 class', () => { + const program = makeTestProgram(SIMPLE_ES2015_CLASS_FILE); + const host = new Esm5ReflectionHost(false, program.getTypeChecker()); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); - beforeEach(() => { - superGetClassSymbolSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassSymbol'); - }); - - it('should return the class symbol returned by the superclass (if any)', () => { - const mockNode = {} as ts.Node; - const mockSymbol = {} as ClassSymbol; - superGetClassSymbolSpy.and.returnValue(mockSymbol); - - const host = new Esm5ReflectionHost(false, {} as any); - - expect(host.getClassSymbol(mockNode)).toBe(mockSymbol); - expect(superGetClassSymbolSpy).toHaveBeenCalledWith(mockNode); + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); }); it('should return the class symbol for an ES5 class (outer variable declaration)', () => { @@ -1572,7 +1573,10 @@ describe('Esm5ReflectionHost', () => { const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const node = getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - expect(host.getClassSymbol(node)).toBeDefined(); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); }); it('should return the class symbol for an ES5 class (inner function declaration)', () => { @@ -1580,27 +1584,22 @@ describe('Esm5ReflectionHost', () => { const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const outerNode = getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = - (((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression) - .expression as ts.FunctionExpression) - .body.statements.find(ts.isFunctionDeclaration) !; + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(innerNode); - expect(host.getClassSymbol(innerNode)).toBeDefined(); + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(outerNode); }); - it('should return the same class symbol (of the inner declaration) for outer and inner declarations', + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', () => { const program = makeTestProgram(SIMPLE_CLASS_FILE); const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const outerNode = getDeclaration( program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = (((outerNode.initializer as ts.ParenthesizedExpression) - .expression as ts.CallExpression) - .expression as ts.FunctionExpression) - .body.statements.find(ts.isFunctionDeclaration) as ClassDeclaration; + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); - expect(host.getClassSymbol(innerNode) !.valueDeclaration).toBe(innerNode); }); it('should return undefined if node is not an ES5 class', () => { @@ -1608,48 +1607,47 @@ describe('Esm5ReflectionHost', () => { const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); - expect(host.getClassSymbol(node)).toBeUndefined(); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); }); }); describe('isClass()', () => { let host: Esm5ReflectionHost; let mockNode: ts.Node; - let superIsClassSpy: jasmine.Spy; - let getClassSymbolSpy: jasmine.Spy; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; beforeEach(() => { host = new Esm5ReflectionHost(false, null as any); mockNode = {} as any; - superIsClassSpy = spyOn(Esm2015ReflectionHost.prototype, 'isClass'); - getClassSymbolSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassSymbol'); + getClassDeclarationSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassDeclaration'); + superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); }); it('should return true if superclass returns true', () => { - superIsClassSpy.and.returnValue(true); + superGetClassDeclarationSpy.and.returnValue(true); + getClassDeclarationSpy.and.callThrough(); expect(host.isClass(mockNode)).toBe(true); - expect(superIsClassSpy).toHaveBeenCalledWith(mockNode); - expect(getClassSymbolSpy).not.toHaveBeenCalled(); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); }); - it('should return true if it can find a symbol for the class', () => { - superIsClassSpy.and.returnValue(false); - getClassSymbolSpy.and.returnValue(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(superIsClassSpy).toHaveBeenCalledWith(mockNode); - expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); }); - it('should return false if it cannot find a symbol for the class', () => { - superIsClassSpy.and.returnValue(false); - getClassSymbolSpy.and.returnValue(false); + it('should return false if it cannot find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(false); expect(host.isClass(mockNode)).toBe(false); - expect(superIsClassSpy).toHaveBeenCalledWith(mockNode); - expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index efb1641690..2c025e7b7a 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -258,28 +258,16 @@ SOME DEFINITION TEXT const {renderer, host, sourceFile, program} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const badSymbolDeclaration = - getDeclaration(program, sourceFile.fileName, 'A', ts.isVariableDeclaration); - const badSymbol: any = {name: 'BadSymbol', declaration: badSymbolDeclaration}; - const hostSpy = spyOn(host, 'getClassSymbol').and.returnValue(null); - expect(() => renderer.addDefinitions(output, badSymbol, 'SOME DEFINITION TEXT')) - .toThrowError('Compiled class does not have a valid symbol: BadSymbol in /some/file.js'); - - const noIifeDeclaration = getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; - hostSpy.and.returnValue({valueDeclaration: noIifeDeclaration}); expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) .toThrowError( 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); - const badIifeWrapper: any = - getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); const badIifeDeclaration = - badIifeWrapper.initializer.expression.expression.body.statements[0]; + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; - hostSpy.and.returnValue({valueDeclaration: badIifeDeclaration}); expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) .toThrowError( 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js');