diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 064ece9fc6..f95d8a4660 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; import {isWithinPackage} from '../analysis/util'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; @@ -108,12 +108,18 @@ 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: + * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * + * or + * + * ``` + * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() + * ``` + * * Here, the intermediate `MyClass_1` assignment is optional. In the above example, the * `class MyClass {}` node is returned as declaration of `MyClass`. * @@ -130,12 +136,18 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } /** - * In ES2015, a class may be declared using a variable declaration of the following structure: + * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * + * or + * + * ``` + * var MyClass = MyClass_1 = (() => { class MyClass {} ... return 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. @@ -145,8 +157,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * 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)) { + // Create a symbol without inner declaration if it is a regular "top level" class declaration. + if (isNamedClassDeclaration(declaration) && isTopLevel(declaration)) { return this.createClassSymbol(declaration, null); } @@ -163,12 +175,18 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } /** - * In ES2015, a class may be declared using a variable declaration of the following structure: + * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * + * or + * + * ``` + * var MyClass = MyClass_1 = (() => { class MyClass {} ... return 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. @@ -178,11 +196,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * of a class. */ protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { - if (!ts.isClassExpression(declaration) || !hasNameIdentifier(declaration)) { + let outerDeclaration: ts.VariableDeclaration|undefined = undefined; + + if (isNamedClassDeclaration(declaration) && !isTopLevel(declaration)) { + let node = declaration.parent; + while (node !== undefined && !ts.isVariableDeclaration(node)) { + node = node.parent; + } + outerDeclaration = node; + } else if (ts.isClassExpression(declaration) && hasNameIdentifier(declaration)) { + outerDeclaration = getVariableDeclarationOfDeclaration(declaration); + } else { return undefined; } - const outerDeclaration = getVariableDeclarationOfDeclaration(declaration); if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) { return undefined; } @@ -745,13 +772,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N /** * Try to retrieve the symbol of a static property on a class. + * + * In some cases, a static property can either be set on the inner declaration inside the class' + * IIFE, or it can be set on the outer variable declaration. Therefore, the host checks both + * places, first looking up the property on the inner symbol, and if the property is not found it + * will fall back to looking up the property on the outer symbol. + * * @param symbol the class whose property we are interested in. * @param propertyName the name of static property. * @returns the symbol if it is found or `undefined` if not. */ protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol |undefined { - return symbol.declaration.exports && symbol.declaration.exports.get(propertyName); + return symbol.implementation.exports && symbol.implementation.exports.get(propertyName) || + symbol.declaration.exports && symbol.declaration.exports.get(propertyName); } /** @@ -1560,7 +1594,14 @@ 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.declaration.valueDeclaration.getSourceFile().statements); + const classNode = classSymbol.implementation.valueDeclaration; + if (isTopLevel(classNode)) { + return this.getModuleStatements(classNode.getSourceFile()); + } else if (ts.isBlock(classNode.parent)) { + return Array.from(classNode.parent.statements); + } + // We should never arrive here + throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`); } /** @@ -2048,6 +2089,38 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass ts.isIdentifier(statement.expression.left); } +/** + * Parse the `expression` that is believed to be an IIFE and return the AST node that corresponds to + * the body of the IIFE. + * + * The expression may be wrapped in parentheses, which are stripped off. + * + * If the IIFE is an arrow function then its body could be a `ts.Expression` rather than a + * `ts.FunctionBody`. + * + * @param expression the expression to parse. + * @returns the `ts.Expression` or `ts.FunctionBody` that holds the body of the IIFE or `undefined` + * if the `expression` did not have the correct shape. + */ +export function getIifeConciseBody(expression: ts.Expression): ts.ConciseBody|undefined { + const call = stripParentheses(expression); + if (!ts.isCallExpression(call)) { + return undefined; + } + + const fn = stripParentheses(call.expression); + if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) { + return undefined; + } + + return fn.body; +} + +/** + * Returns true if the `node` is an assignment of the form `a = b`. + * + * @param node The AST node to check. + */ export function isAssignment(node: ts.Node): node is ts.AssignmentExpression { return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken; } @@ -2132,12 +2205,18 @@ function getCalleeName(call: ts.CallExpression): string|null { ///////////// Internal Helpers ///////////// /** - * In ES2015, a class may be declared using a variable declaration of the following structure: + * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * + * or + * + * ``` + * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() + * ``` + * * Here, the intermediate `MyClass_1` assignment is optional. In the above example, the * `class MyClass {}` expression is returned as declaration of `var MyClass`. If the variable * is not initialized using a class expression, null is returned. @@ -2145,23 +2224,40 @@ function getCalleeName(call: ts.CallExpression): string|null { * @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 { +function getInnerClassDeclaration(node: ts.Node): + ClassDeclaration|null { if (!ts.isVariableDeclaration(node) || node.initializer === undefined) { return 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; + if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) { + return expression; } - return expression; + // Try to parse out a class declaration wrapped in an IIFE (as generated by TS 3.9) + // e.g. + // /* @class */ = (() => { + // class MyClass {} + // ... + // return MyClass; + // })(); + const iifeBody = getIifeConciseBody(expression); + if (iifeBody === undefined) { + return null; + } + // Extract the class declaration from inside the IIFE. + const innerDeclaration = ts.isBlock(iifeBody) ? + iifeBody.statements.find(ts.isClassDeclaration) : + ts.isClassExpression(iifeBody) ? iifeBody : undefined; + if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) { + return null; + } + return innerDeclaration; } function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { @@ -2208,6 +2304,16 @@ function isClassMemberType(node: ts.Declaration): node is ts.ClassElement| * var MyClass = MyClass_1 = class MyClass {}; * ``` * + * or + * + * ``` + * var MyClass = MyClass_1 = (() => { + * class MyClass {} + * ... + * return MyClass; + * })() + ``` + * * and the provided declaration being `class MyClass {}`, this will return the `var MyClass` * declaration. * @@ -2301,3 +2407,12 @@ function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] { return bundle.program.getSourceFiles().filter( f => (f !== rootFile) && isWithinPackage(bundle.package, f)); } + +function isTopLevel(node: ts.Node): boolean { + while (node = node.parent) { + if (ts.isBlock(node)) { + return false; + } + } + return true; +} diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 43d3333293..f620e38243 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -11,9 +11,8 @@ import * as ts from 'typescript'; import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, isNamedVariableDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {getNameText, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils'; -import {Esm2015ReflectionHost, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; +import {Esm2015ReflectionHost, getIifeConciseBody, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; -import {stripParentheses} from './utils'; /** @@ -489,30 +488,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent; return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : []; } - - /** - * Try to retrieve the symbol of a static property on a class. - * - * In ES5, a static property can either be set on the inner function declaration inside the class' - * IIFE, or it can be set on the outer variable declaration. Therefore, the ES5 host checks both - * places, first looking up the property on the inner symbol, and if the property is not found it - * will fall back to looking up the property on the outer symbol. - * - * @param symbol the class whose property we are interested in. - * @param propertyName the name of static property. - * @returns the symbol if it is found or `undefined` if not. - */ - protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol - |undefined { - // 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, lookup the static properties on the outer class symbol. - return symbol.declaration.exports && symbol.declaration.exports.get(propertyName); - } } ///////////// Internal Helpers ///////////// @@ -631,17 +606,8 @@ export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined { parenthesizedCall = parenthesizedCall.right; } - const call = stripParentheses(parenthesizedCall); - if (!ts.isCallExpression(call)) { - return undefined; - } - - const fn = stripParentheses(call.expression); - if (!ts.isFunctionExpression(fn)) { - return undefined; - } - - return fn.body; + const body = getIifeConciseBody(parenthesizedCall); + return body !== undefined && ts.isBlock(body) ? body : undefined; } function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined { 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 37c9f769a5..8d3fbb580a 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -30,6 +30,7 @@ runInEachFileSystem(() => { let ACCESSORS_FILE: TestFile; let SIMPLE_CLASS_FILE: TestFile; let CLASS_EXPRESSION_FILE: TestFile; + let WRAPPED_CLASS_EXPRESSION_FILE: TestFile; let FOO_FUNCTION_FILE: TestFile; let INVALID_DECORATORS_FILE: TestFile; let INVALID_DECORATOR_ARGS_FILE: TestFile; @@ -150,6 +151,26 @@ runInEachFileSystem(() => { `, }; + WRAPPED_CLASS_EXPRESSION_FILE = { + name: _('/wrapped_class_expression.js'), + contents: ` + import {Directive} from '@angular/core'; + var AliasedWrappedClass_1; + let SimpleWrappedClass = /** @class */ (() => { + class SimpleWrappedClass {} + return SimpleWrappedClass; + })(); + let AliasedWrappedClass = AliasedWrappedClass_1 = /** @class */ (() => { + class AliasedWrappedClass {} + AliasedWrappedClass.decorators = [ + { type: Directive, args: [{ selector: '[someDirective]' },] } + ]; + return AliasedWrappedClass; + })(); + let usageOfWrappedClass = AliasedWrappedClass_1; + `, + }; + FOO_FUNCTION_FILE = { name: _('/foo_function.js'), contents: ` @@ -762,6 +783,26 @@ runInEachFileSystem(() => { ]); }); + it('should find the decorators on an aliased wrapped class', () => { + loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]); + const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const classNode = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode)!; + + expect(decorators).not.toBe(null!); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args!.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + it('should return null if the symbol is not a class', () => { loadTestFiles([FOO_FUNCTION_FILE]); const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name); @@ -1620,6 +1661,22 @@ runInEachFileSystem(() => { .toBe(classDeclaration); }); + it('should return the original declaration of an aliased class', () => { + loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]); + const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const classDeclaration = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass', + ts.isVariableDeclaration); + const usageOfWrappedClass = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'usageOfWrappedClass', + ts.isVariableDeclaration); + const aliasedClassIdentifier = usageOfWrappedClass.initializer as ts.Identifier; + expect(aliasedClassIdentifier.text).toBe('AliasedWrappedClass_1'); + expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier)!.node) + .toBe(classDeclaration); + }); + it('should recognize enum declarations with string values', () => { const testFile: TestFile = { name: _('/node_modules/test-package/some/file.js'), @@ -1832,6 +1889,70 @@ runInEachFileSystem(() => { expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); + it('should return the class symbol for a wrapped class expression (outer variable declaration)', + () => { + loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]); + const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const outerNode = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(outerNode); + + if (classSymbol === undefined) { + return fail('Expected classSymbol to be defined'); + } + expect(classSymbol.name).toEqual('SimpleWrappedClass'); + expect(classSymbol.declaration.valueDeclaration).toBe(outerNode); + if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) { + return fail('Expected a named class declaration'); + } + expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); + }); + + it('should return the class symbol for a wrapped class expression (inner class expression)', + () => { + loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]); + const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const outerNode = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block) + .statements[0]; + const classSymbol = host.getClassSymbol(innerNode); + + if (classSymbol === undefined) { + return fail('Expected classSymbol to be defined'); + } + expect(classSymbol.name).toEqual('SimpleWrappedClass'); + expect(classSymbol.declaration.valueDeclaration).toBe(outerNode); + if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) { + return fail('Expected a named class declaration'); + } + expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); + }); + + it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations', + () => { + loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]); + const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const outerNode = getDeclaration( + bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block) + .statements[0]; + + 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 bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);