From d7440c452a803b14383feeb71ee188b4f08beb02 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 12 May 2020 08:20:00 +0100 Subject: [PATCH] fix(ngcc): ensure reflection hosts can handle TS 3.9 IIFE wrapped classes (#36989) In TS 3.9, ES2015 output can contain ES classes that are wrapped in an IIFE. So now ES2015 class declarations can look like one of: ``` class OuterClass1 {} ``` ``` let OuterClass = class InnerClass {}; ``` ``` var AliasClass; let OuterClass = AliasClass = class InnerClass {}; ``` ``` let OuterClass = (() => class InnerClass {}}; ``` ``` var AliasClass; let OuterClass = AliasClass = (() => class InnerClass {})(); ``` ``` let OuterClass = (() => { let AdjacentClass = class InnerClass {}; // ... static properties or decorators attached to `AdjacentClass` return AdjacentClass; })(); ``` ``` var AliasClass; let OuterClass = AliasClass = (() => { let AdjacentClass = class InnerClass {}; // ... static properties or decorators attached to `AdjacentClass` return AdjacentClass; })(); ``` The `Esm5ReflectionHost` already handles slightly different IIFE wrappers around function-based classes. This can be substantially reused when fixing `Esm2015ReflectionHost`, since there is a lot of commonality between the two. This commit moves code from the `Esm5ReflectionHost` into the `Esm2015ReflectionHost` and looks to share as much as possible between the two hosts. PR Close #36989 --- .../ngcc/src/host/esm2015_host.ts | 667 ++++++++++++------ .../compiler-cli/ngcc/src/host/esm5_host.ts | 312 ++------ .../compiler-cli/ngcc/src/host/ngcc_host.ts | 6 + .../ngcc/test/host/commonjs_host_spec.ts | 20 +- .../ngcc/test/host/esm2015_host_spec.ts | 206 +++++- .../test/host/esm5_host_import_helper_spec.ts | 6 +- .../ngcc/test/host/esm5_host_spec.ts | 22 +- .../ngcc/test/host/umd_host_spec.ts | 20 +- 8 files changed, 792 insertions(+), 467 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index b951a6f597..1936783d1d 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, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, 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'; @@ -135,122 +135,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return this.getClassSymbolFromInnerDeclaration(declaration); } - /** - * 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. - * - * @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 it is a regular "top level" class declaration. - if (isNamedClassDeclaration(declaration) && isTopLevel(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 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. - * - * @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 { - 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; - } - - 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, - }; - } - /** * Examine a declaration (for example, of a class or function) and return metadata about any * decorators present on the declaration. @@ -319,17 +203,60 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { - // First try getting the base class from the "outer" declaration + // First try getting the base class from an ES2015 class declaration const superBaseClassIdentifier = super.getBaseClassExpression(clazz); if (superBaseClassIdentifier) { return superBaseClassIdentifier; } + // That didn't work so now try getting it from the "inner" declaration. - const innerClassDeclaration = getInnerClassDeclaration(clazz); - if (innerClassDeclaration === null) { + const classSymbol = this.getClassSymbol(clazz); + if (classSymbol === undefined || + !isNamedDeclaration(classSymbol.implementation.valueDeclaration)) { return null; } - return super.getBaseClassExpression(innerClassDeclaration); + return super.getBaseClassExpression(classSymbol.implementation.valueDeclaration); + } + + getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier { + const classSymbol = this.getClassSymbol(clazz); + if (classSymbol === undefined) { + throw new Error(`getInternalNameOfClass() called on a non-class: expected ${ + clazz.name.text} to be a class declaration.`); + } + return this.getNameFromClassSymbolDeclaration( + classSymbol, classSymbol.implementation.valueDeclaration); + } + + getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier { + const classSymbol = this.getClassSymbol(clazz); + if (classSymbol === undefined) { + throw new Error(`getAdjacentNameOfClass() called on a non-class: expected ${ + clazz.name.text} to be a class declaration.`); + } + + if (classSymbol.adjacent !== undefined) { + return this.getNameFromClassSymbolDeclaration( + classSymbol, classSymbol.adjacent.valueDeclaration); + } else { + return this.getNameFromClassSymbolDeclaration( + classSymbol, classSymbol.implementation.valueDeclaration); + } + } + + private getNameFromClassSymbolDeclaration( + classSymbol: NgccClassSymbol, declaration: ts.Declaration): ts.Identifier { + if (declaration === undefined) { + throw new Error( + `getInternalNameOfClass() called on a class with an undefined internal declaration. External class name: ${ + classSymbol.name}; internal class name: ${classSymbol.implementation.name}.`); + } + if (!isNamedDeclaration(declaration)) { + throw new Error( + `getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${ + declaration.getText()}`); + } + return declaration.name; } /** @@ -368,22 +295,30 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return superDeclaration; } + const outerClassNode = getClassDeclarationFromInnerDeclaration(superDeclaration.node); + const declaration = outerClassNode !== null ? + this.getDeclarationOfIdentifier(outerClassNode.name) : + superDeclaration; + if (declaration === null || declaration.node === null || declaration.known !== null) { + return declaration; + } + // The identifier may have been of an additional class assignment such as `MyClass_1` that was // present as alias for `MyClass`. If so, resolve such aliases to their original declaration. - const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node); + const aliasedIdentifier = this.resolveAliasedClassIdentifier(declaration.node); if (aliasedIdentifier !== null) { return this.getDeclarationOfIdentifier(aliasedIdentifier); } // Variable declarations may represent an enum declaration, so attempt to resolve its members. - if (ts.isVariableDeclaration(superDeclaration.node)) { - const enumMembers = this.resolveEnumMembers(superDeclaration.node); + if (ts.isVariableDeclaration(declaration.node)) { + const enumMembers = this.resolveEnumMembers(declaration.node); if (enumMembers !== null) { - superDeclaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; + declaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; } } - return superDeclaration; + return declaration; } /** @@ -555,31 +490,47 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } getEndOfClass(classSymbol: NgccClassSymbol): ts.Node { - let last: ts.Node = classSymbol.declaration.valueDeclaration; + const implementation = classSymbol.implementation; + let last: ts.Node = implementation.valueDeclaration; + const implementationStatement = getContainingStatement(last); + if (implementationStatement === null) return last; - // If there are static members on this class then find the last one - if (classSymbol.declaration.exports !== undefined) { - classSymbol.declaration.exports.forEach(exportSymbol => { - if (exportSymbol.valueDeclaration === undefined) { - return; - } - const exportStatement = getContainingStatement(exportSymbol.valueDeclaration); - if (exportStatement !== null && last.getEnd() < exportStatement.getEnd()) { - last = exportStatement; + const container = implementationStatement.parent; + if (ts.isBlock(container)) { + // Assume that the implementation is inside an IIFE + const returnStatementIndex = container.statements.findIndex(ts.isReturnStatement); + if (returnStatementIndex === -1) { + throw new Error( + `Compiled class wrapper IIFE does not have a return statement: ${classSymbol.name} in ${ + classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`); + } + + // Return the statement before the IIFE return statement + last = container.statements[returnStatementIndex - 1]; + } else if (ts.isSourceFile(container)) { + // If there are static members on this class then find the last one + if (implementation.exports !== undefined) { + implementation.exports.forEach(exportSymbol => { + if (exportSymbol.valueDeclaration === undefined) { + return; + } + const exportStatement = getContainingStatement(exportSymbol.valueDeclaration); + if (exportStatement !== null && last.getEnd() < exportStatement.getEnd()) { + last = exportStatement; + } + }); + } + + // If there are helper calls for this class then find the last one + const helpers = this.getHelperCallsForClass( + classSymbol, ['__decorate', '__extends', '__param', '__metadata']); + helpers.forEach(helper => { + const helperStatement = getContainingStatement(helper); + if (helperStatement !== null && last.getEnd() < helperStatement.getEnd()) { + last = helperStatement; } }); } - - // If there are helper calls for this class then find the last one - const helpers = this.getHelperCallsForClass( - classSymbol, ['__decorate', '__extends', '__param', '__metadata']); - helpers.forEach(helper => { - const helperStatement = getContainingStatement(helper); - if (helperStatement !== null && last.getEnd() < helperStatement.getEnd()) { - last = helperStatement; - } - }); - return last; } @@ -602,6 +553,186 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N ///////////// Protected Helpers ///////////// + /** + * A class may be declared as a top level class declaration: + * + * ``` + * class OuterClass { ... } + * ``` + * + * or in a variable declaration to a class expression: + * + * ``` + * var OuterClass = ClassAlias = class InnerClass {}; + * ``` + * + * or in a variable declaration to an IIFE containing a class declaration + * + * ``` + * var OuterClass = ClassAlias = (() => { + * class InnerClass {} + * ... + * return InnerClass; + * })() + * ``` + * + * or in a variable declaration to an IIFE containing a function declaration + * + * ``` + * var OuterClass = ClassAlias = (() => { + * function InnerClass() {} + * ... + * return InnerClass; + * })() + * ``` + * + * This method returns an `NgccClassSymbol` when provided with one of these cases. + * + * @param declaration the declaration whose symbol we are finding. + * @returns the symbol for the class or `undefined` if `declaration` does not represent an outer + * declaration of a class. + */ + protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + // Return a class symbol without an inner declaration if it is a regular "top level" class + if (isNamedClassDeclaration(declaration) && isTopLevel(declaration)) { + return this.createClassSymbol(declaration, null); + } + + // Otherwise, an outer class declaration must be an initialized variable declaration: + if (!isInitializedVariableClassDeclaration(declaration)) { + return undefined; + } + + const innerDeclaration = getInnerClassDeclaration(skipClassAliases(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 structures: + * + * ``` + * let MyClass = MyClass_1 = class MyClass {}; + * ``` + * + * or + * + * ``` + * let MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() + * ``` + * + * or + * + * ``` + * let MyClass = MyClass_1 = (() => { let MyClass = 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. + * + * @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 { + let outerDeclaration: ts.ClassDeclaration|ts.VariableDeclaration|undefined = undefined; + + if (ts.isClassExpression(declaration) && hasNameIdentifier(declaration)) { + // Handle `let MyClass = MyClass_1 = class MyClass {};` + outerDeclaration = getFarLeftHandSideOfAssignment(declaration); + + // Handle this being in an IIFE + if (outerDeclaration !== undefined && !isTopLevel(outerDeclaration)) { + outerDeclaration = getContainingVariableDeclaration(outerDeclaration); + } + } else if (isNamedClassDeclaration(declaration)) { + // Handle `class MyClass {}` statement + if (isTopLevel(declaration)) { + // At the top level + outerDeclaration = declaration; + } else { + // Or inside an IIFE + outerDeclaration = getContainingVariableDeclaration(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: ts.Node|null): + NgccClassSymbol|undefined { + const declarationSymbol = + this.checker.getSymbolAtLocation(outerDeclaration.name) as ClassSymbol | undefined; + if (declarationSymbol === undefined) { + return undefined; + } + + let implementationSymbol = declarationSymbol; + if (innerDeclaration !== null && isNamedDeclaration(innerDeclaration)) { + implementationSymbol = this.checker.getSymbolAtLocation(innerDeclaration.name) as ClassSymbol; + } + + if (implementationSymbol === undefined) { + return undefined; + } + + const classSymbol: NgccClassSymbol = { + name: declarationSymbol.name, + declaration: declarationSymbol, + implementation: implementationSymbol, + }; + + let adjacent = this.getAdjacentSymbol(declarationSymbol, implementationSymbol); + if (adjacent !== null) { + classSymbol.adjacent = adjacent; + } + + return classSymbol; + } + + private getAdjacentSymbol(declarationSymbol: ClassSymbol, implementationSymbol: ClassSymbol): + ClassSymbol|undefined { + if (declarationSymbol === implementationSymbol) { + return undefined; + } + const innerDeclaration = implementationSymbol.valueDeclaration; + if (!ts.isClassExpression(innerDeclaration) && !ts.isFunctionExpression(innerDeclaration)) { + return undefined; + } + // Deal with the inner class looking like this inside an IIFE: + // `let MyClass = class MyClass {};` or `var MyClass = function MyClass() {};` + const adjacentDeclaration = getFarLeftHandSideOfAssignment(innerDeclaration); + if (adjacentDeclaration === undefined || !isNamedVariableDeclaration(adjacentDeclaration)) { + return undefined; + } + const adjacentSymbol = + this.checker.getSymbolAtLocation(adjacentDeclaration.name) as ClassSymbol; + if (adjacentSymbol === declarationSymbol || adjacentSymbol === implementationSymbol) { + return undefined; + } + return adjacentSymbol; + } + /** * Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known * declaration. @@ -611,7 +742,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const declaration = super.getDeclarationOfSymbol(symbol, originalId); if (declaration === null) { return null; - } + } return this.detectKnownDeclaration(declaration); } @@ -892,6 +1023,33 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } } + // If this class was declared as a VariableDeclaration inside an IIFE, then it may have static + // properties attached to the variable rather than the class itself. + // + // For example: + // ``` + // let OuterClass = (() => { + // let AdjacentClass = class InternalClass { + // // no static properties here! + // } + // AdjacentClass.staticProperty = ...; + // })(); + // ``` + if (symbol.adjacent !== undefined) { + if (ts.isVariableDeclaration(symbol.adjacent.valueDeclaration)) { + if (symbol.adjacent.exports !== undefined) { + symbol.adjacent.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({ @@ -991,15 +1149,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const outerDeclaration = classSymbol.declaration.valueDeclaration; const innerDeclaration = classSymbol.implementation.valueDeclaration; + const adjacentDeclaration = + this.getAdjacentNameOfClass((classSymbol.declaration.valueDeclaration)).parent; const matchesClass = (identifier: ts.Identifier) => { const decl = this.getDeclarationOfIdentifier(identifier); - if (decl === null) { - return false; - } - - // The identifier corresponds with the class if its declaration is either the outer or inner - // declaration. - return decl.node === outerDeclaration || decl.node === innerDeclaration; + return decl !== null && + (decl.node === adjacentDeclaration || decl.node === outerDeclaration || + decl.node === innerDeclaration); }; for (const helperCall of helperCalls) { @@ -1545,8 +1701,10 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N 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); + } + const statement = getContainingStatement(classNode); + if (ts.isBlock(statement.parent)) { + return Array.from(statement.parent.statements); } // We should never arrive here throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`); @@ -1991,7 +2149,7 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass * @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 { +export function getIifeBody(expression: ts.Expression): ts.ConciseBody|undefined { const call = stripParentheses(expression); if (!ts.isCallExpression(call)) { return undefined; @@ -2093,60 +2251,107 @@ function getCalleeName(call: ts.CallExpression): string|null { ///////////// Internal Helpers ///////////// +type InitializedVariableClassDeclaration = + ClassDeclaration&{initializer: ts.Expression}; + +function isInitializedVariableClassDeclaration(node: ts.Node): + node is InitializedVariableClassDeclaration { + return isNamedVariableDeclaration(node) && node.initializer !== undefined; +} /** - * In ES2015, a class may be declared using a variable declaration of the following structures: + * Handle a variable declaration of the form * * ``` - * var MyClass = MyClass_1 = class MyClass {}; + * var MyClass = alias1 = alias2 = <> * ``` * - * 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. - * - * @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". + * @node the LHS of a variable declaration. + * @returns the original AST node or the RHS of a series of assignments in a variable + * declaration. */ -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 {};` +export function skipClassAliases(node: InitializedVariableClassDeclaration): ts.Expression { let expression = node.initializer; while (isAssignment(expression)) { expression = expression.right; } + return expression; +} + +/** + * This expression could either be a class expression + * + * ``` + * class MyClass {}; + * ``` + * + * or an IIFE wrapped class expression + * + * ``` + * (() => { + * class MyClass {} + * ... + * return MyClass; + * })() + * ``` + * + * or an IIFE wrapped aliased class expression + * + * ``` + * (() => { + * let MyClass = class MyClass {} + * ... + * return MyClass; + * })() + * ``` + * + * or an IFFE wrapped ES5 class function + * + * ``` + * (function () { + * function MyClass() {} + * ... + * return MyClass + * })() + * ``` + * + * @param expression 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(expression: ts.Expression): + ClassDeclaration|null { if (ts.isClassExpression(expression) && hasNameIdentifier(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); + const iifeBody = getIifeBody(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; + + if (!ts.isBlock(iifeBody)) { + // Handle the fat arrow expression case: `() => ClassExpression` + return ts.isClassExpression(iifeBody) && isNamedDeclaration(iifeBody) ? iifeBody : null; + } else { + // Handle the case of a normal or fat-arrow function with a body. + // Return the first ClassDeclaration/VariableDeclaration inside the body + for (const statement of iifeBody.statements) { + if (isNamedClassDeclaration(statement) || isNamedFunctionDeclaration(statement)) { + return statement; + } + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if (isInitializedVariableClassDeclaration(declaration)) { + const expression = skipClassAliases(declaration); + if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) { + return expression; + } + } + } + } + } } - return innerDeclaration; + + return null; } function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { @@ -2170,8 +2375,7 @@ function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression& node.left.expression.kind === ts.SyntaxKind.ThisKeyword; } -function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration& - {name: ts.Identifier} { +function isNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration&{name: ts.Identifier} { const anyNode: any = node; return !!anyNode.name && ts.isIdentifier(anyNode.name); } @@ -2209,7 +2413,7 @@ function isClassMemberType(node: ts.Declaration): node is ts.ClassElement| * @param declaration The declaration for which any variable declaration should be obtained. * @returns the outer variable declaration if found, undefined otherwise. */ -function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.VariableDeclaration| +function getFarLeftHandSideOfAssignment(declaration: ts.Declaration): ts.VariableDeclaration| undefined { let node = declaration.parent; @@ -2221,6 +2425,18 @@ function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.Va return ts.isVariableDeclaration(node) ? node : undefined; } +function getContainingVariableDeclaration(node: ts.Node): ClassDeclaration| + undefined { + node = node.parent; + while (node !== undefined) { + if (isNamedVariableDeclaration(node)) { + return node; + } + node = node.parent; + } + return undefined; +} + /** * A constructor function may have been "synthesized" by TypeScript during JavaScript emit, * in the case no user-defined constructor exists and e.g. property initializers are used. @@ -2273,14 +2489,14 @@ function isSynthesizedSuperCall(expression: ts.Expression): boolean { * Find the statement that contains the given node * @param node a node whose containing statement we wish to find */ -function getContainingStatement(node: ts.Node): ts.ExpressionStatement|null { - while (node) { - if (ts.isExpressionStatement(node)) { +function getContainingStatement(node: ts.Node): ts.Statement { + while (node.parent) { + if (ts.isBlock(node.parent) || ts.isSourceFile(node.parent)) { break; } node = node.parent; } - return node || null; + return node as ts.Statement; } function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile { @@ -2305,3 +2521,52 @@ function isTopLevel(node: ts.Node): boolean { } return true; } + +/** + * Get the actual (outer) declaration of a 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 inner function declaration, we want to get to the declaration of the outer variable + * that represents the class. + * + * @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". + */ +export function getClassDeclarationFromInnerDeclaration(node: ts.Node): + ClassDeclaration|null { + if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) { + // It might be the function expression inside the IIFE. We need to go 5 levels up... + + // - IIFE body. + let outerNode = node.parent; + if (!outerNode || !ts.isBlock(outerNode)) return null; + + // - IIFE function expression. + outerNode = outerNode.parent; + if (!outerNode || (!ts.isFunctionExpression(outerNode) && !ts.isArrowFunction(outerNode))) { + return null; + } + outerNode = outerNode.parent; + + // - Parenthesis inside IIFE. + if (outerNode && ts.isParenthesizedExpression(outerNode)) outerNode = outerNode.parent; + + // - IIFE call expression. + if (!outerNode || !ts.isCallExpression(outerNode)) return null; + outerNode = outerNode.parent; + + // - Parenthesis around IIFE. + if (outerNode && ts.isParenthesizedExpression(outerNode)) outerNode = outerNode.parent; + + // - Outer variable declaration. + if (!outerNode || !ts.isVariableDeclaration(outerNode)) return null; + + // Finally, ensure that the variable declaration has a `name` identifier. + return hasNameIdentifier(outerNode) ? outerNode : null; + } + + return null; +} diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index fb2d7e0e01..62bc49c824 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -8,10 +8,10 @@ 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 {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; +import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils'; -import {Esm2015ReflectionHost, getIifeConciseBody, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; +import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; @@ -23,6 +23,7 @@ import {NgccClassSymbol} from './ngcc_host'; * function CommonModule() { * } * CommonModule.decorators = [ ... ]; + * return CommonModule; * ``` * * * "Classes" are decorated if they have a static property called `decorators`. @@ -34,16 +35,13 @@ import {NgccClassSymbol} from './ngcc_host'; */ export class Esm5ReflectionHost extends Esm2015ReflectionHost { getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { - const classSymbol = this.getClassSymbol(clazz); - if (classSymbol === undefined) { - return null; + const superBaseClassExpression = super.getBaseClassExpression(clazz); + if (superBaseClassExpression !== null) { + return superBaseClassExpression; } - const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration); - if (!iifeBody) return null; - - const iife = iifeBody.parent; - if (!iife || !ts.isFunctionExpression(iife)) return null; + const iife = getIifeFn(this.getClassSymbol(clazz)); + if (iife === null) return null; if (iife.parameters.length !== 1 || !isSuperIdentifier(iife.parameters[0].name)) { return null; @@ -56,100 +54,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { return iife.parent.arguments[0]; } - getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier { - const innerClass = this.getInnerFunctionDeclarationFromClassDeclaration(clazz); - if (innerClass === undefined) { - throw new Error(`getInternalNameOfClass() called on a non-ES5 class: expected ${ - clazz.name.text} to have an inner class declaration`); - } - if (innerClass.name === undefined) { - throw new Error( - `getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${ - innerClass.getText()}`); - } - return innerClass.name; - } - - getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier { - return this.getInternalNameOfClass(clazz); - } - - getEndOfClass(classSymbol: NgccClassSymbol): ts.Node { - const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration); - if (!iifeBody) { - throw new Error(`Compiled class declaration is not inside an IIFE: ${classSymbol.name} in ${ - classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`); - } - - const returnStatementIndex = iifeBody.statements.findIndex(ts.isReturnStatement); - if (returnStatementIndex === -1) { - throw new Error( - `Compiled class wrapper IIFE does not have a return statement: ${classSymbol.name} in ${ - classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`); - } - - // Return the statement before the IIFE return statement - return iifeBody.statements[returnStatementIndex - 1]; - } - - /** - * 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 outer variable which is - * assigned the result of 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 getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { - const classSymbol = super.getClassSymbolFromOuterDeclaration(declaration); - if (classSymbol !== undefined) { - return classSymbol; - } - - if (!isNamedVariableDeclaration(declaration)) { - return undefined; - } - - 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 === null || !hasNameIdentifier(outerDeclaration)) { - return undefined; - } - - return this.createClassSymbol(outerDeclaration, declaration); - } - /** * Trace an identifier to its declaration, if possible. * @@ -168,9 +72,9 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { * otherwise. */ getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { - const superDeclaration = super.getDeclarationOfIdentifier(id); + const declaration = super.getDeclarationOfIdentifier(id); - if (superDeclaration === null) { + if (declaration === null) { const nonEmittedNorImportedTsHelperDeclaration = getTsHelperFnFromIdentifier(id); if (nonEmittedNorImportedTsHelperDeclaration !== null) { // No declaration could be found for this identifier and its name matches a known TS helper @@ -186,17 +90,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { } } - if (superDeclaration === null || superDeclaration.node === null || - superDeclaration.known !== null) { - return superDeclaration; - } - - // Get the identifier for the outer class node (if any). - const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(superDeclaration.node); - const declaration = outerClassNode !== null ? - super.getDeclarationOfIdentifier(outerClassNode.name) : - superDeclaration; - if (declaration === null || declaration.node === null || declaration.known !== null) { return declaration; } @@ -235,23 +128,24 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { * @returns an object containing the node, statements and parameters of the function. */ getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { - if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) && - !ts.isFunctionExpression(node)) { + const definition = super.getDefinitionOfFunction(node); + if (definition === null) { return null; } - const parameters = - node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null})); - let lookingForParamInitializers = true; + // Filter out and capture parameter initializers + if (definition.body !== null) { + let lookingForInitializers = true; + const statements = definition.body.filter(s => { + lookingForInitializers = + lookingForInitializers && captureParamInitializer(s, definition.parameters); + // If we are no longer looking for parameter initializers then we include this statement + return !lookingForInitializers; + }); + definition.body = statements; + } - const statements = node.body && node.body.statements.filter(s => { - lookingForParamInitializers = - lookingForParamInitializers && reflectParamInitializer(s, parameters); - // If we are no longer looking for parameter initializers then we include this statement - return !lookingForParamInitializers; - }); - - return {node, body: statements || null, parameters}; + return definition; } /** @@ -276,38 +170,32 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { ///////////// 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, + * 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. * - * 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. + * This method extracts a `NgccClassSymbol` if `declaration` is the function declaration inside + * the IIFE. Otherwise, undefined is returned. * - * Given the outer variable declaration, we want to get to the inner function declaration. - * - * @param decl a declaration 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". + * @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 getInnerFunctionDeclarationFromClassDeclaration(decl: ts.Declaration): - ts.FunctionDeclaration|undefined { - // Extract the IIFE body (if any). - const iifeBody = getIifeBody(decl); - if (!iifeBody) return undefined; + protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { + const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration); + if (classSymbol !== undefined) { + return classSymbol; + } - // Extract the function declaration from inside the IIFE. - const functionDeclaration = iifeBody.statements.find(ts.isFunctionDeclaration); - if (!functionDeclaration) return undefined; + if (!ts.isFunctionDeclaration(declaration) || !hasNameIdentifier(declaration)) { + return undefined; + } - // Extract the return identifier of the IIFE. - const returnIdentifier = getReturnIdentifier(iifeBody); - const returnIdentifierSymbol = - returnIdentifier && this.checker.getSymbolAtLocation(returnIdentifier); - if (!returnIdentifierSymbol) return undefined; + const outerDeclaration = getClassDeclarationFromInnerDeclaration(declaration); + if (outerDeclaration === null || !hasNameIdentifier(outerDeclaration)) { + return undefined; + } - // Verify that the inner function is returned. - if (returnIdentifierSymbol.valueDeclaration !== functionDeclaration) return undefined; - - return functionDeclaration; + return this.createClassSymbol(outerDeclaration, declaration); } /** @@ -524,83 +412,6 @@ function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name return property && ts.isFunctionExpression(property.initializer) && property.initializer || null; } -/** - * Get the actual (outer) declaration of a 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 inner function declaration, we want to get to the declaration of the outer variable - * that represents the class. - * - * @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): - ClassDeclaration|null { - if (ts.isFunctionDeclaration(node)) { - // It might be the function expression inside the IIFE. We need to go 5 levels up... - - // 1. IIFE body. - let outerNode = node.parent; - if (!outerNode || !ts.isBlock(outerNode)) return null; - - // 2. IIFE function expression. - outerNode = outerNode.parent; - if (!outerNode || !ts.isFunctionExpression(outerNode)) return null; - - // 3. IIFE call expression. - outerNode = outerNode.parent; - if (!outerNode || !ts.isCallExpression(outerNode)) return null; - - // 4. Parenthesis around IIFE. - outerNode = outerNode.parent; - if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return null; - - // 5. Outer variable declaration. - outerNode = outerNode.parent; - if (!outerNode || !ts.isVariableDeclaration(outerNode)) return null; - - // Finally, ensure that the variable declaration has a `name` identifier. - return hasNameIdentifier(outerNode) ? outerNode : null; - } - - return null; -} - -export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined { - if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) { - return undefined; - } - - // Recognize a variable declaration of one of the forms: - // - `var MyClass = (function () { ... }());` - // - `var MyClass = MyClass_1 = (function () { ... }());` - let parenthesizedCall = declaration.initializer; - while (isAssignment(parenthesizedCall)) { - parenthesizedCall = parenthesizedCall.right; - } - - const body = getIifeConciseBody(parenthesizedCall); - return body !== undefined && ts.isBlock(body) ? body : undefined; -} - -function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined { - const returnStatement = body.statements.find(ts.isReturnStatement); - if (!returnStatement || !returnStatement.expression) { - return undefined; - } - if (ts.isIdentifier(returnStatement.expression)) { - return returnStatement.expression; - } - if (isAssignment(returnStatement.expression) && - ts.isIdentifier(returnStatement.expression.left)) { - return returnStatement.expression.left; - } - return undefined; -} - function getReturnStatement(declaration: ts.Expression|undefined): ts.ReturnStatement|undefined { return declaration && ts.isFunctionExpression(declaration) ? declaration.body.statements.find(ts.isReturnStatement) : @@ -766,7 +577,7 @@ function isSuperIdentifier(node: ts.Node): boolean { * @param parameters the collection of parameters that were found in the function definition * @returns true if the statement was a parameter initializer */ -function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) { +function captureParamInitializer(statement: ts.Statement, parameters: Parameter[]) { if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) && ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) { const ifStatementComparison = statement.expression; // (arg === void 0) @@ -792,3 +603,34 @@ function isUndefinedComparison(expression: ts.Expression): expression is ts.Expr expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken && ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left); } + +/** + * Parse the declaration of the given `classSymbol` to find the IIFE wrapper function. + * + * This function may accept a `_super` argument if there is a base class. + * + * ``` + * var TestClass = (function (_super) { + * __extends(TestClass, _super); + * function TestClass() {} + * return TestClass; + * }(BaseClass)); + * ``` + * + * @param classSymbol the class whose iife wrapper function we want to get. + * @returns the IIFE function or null if it could not be parsed. + */ +function getIifeFn(classSymbol: NgccClassSymbol|undefined): ts.FunctionExpression|null { + if (classSymbol === undefined) { + return null; + } + + const innerDeclaration = classSymbol.implementation.valueDeclaration; + const iifeBody = innerDeclaration.parent; + if (!ts.isBlock(iifeBody)) { + return null; + } + + const iifeWrapper = iifeBody.parent; + return iifeWrapper && ts.isFunctionExpression(iifeWrapper) ? iifeWrapper : null; +} diff --git a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts index a6165e7bb4..8c1f31b981 100644 --- a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts @@ -48,6 +48,12 @@ export interface NgccClassSymbol { * declaration. */ implementation: ts.Symbol; + + /** + * Represents the symbol corresponding to a variable within a class IIFE that may be used to + * attach static properties or decorated. + */ + adjacent?: ts.Symbol; } /** 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 3ead77cd2c..af729354dc 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -14,7 +14,7 @@ import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; -import {getIifeBody} from '../../src/host/esm5_host'; +import {getIifeBody} from '../../src/host/esm2015_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {BundleProgram} from '../../src/packages/bundle_program'; import {MockLogger} from '../helpers/mock_logger'; @@ -2212,7 +2212,8 @@ exports.MissingClass2 = MissingClass2; createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2227,7 +2228,8 @@ exports.MissingClass2 = MissingClass2; createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); @@ -2243,7 +2245,8 @@ exports.MissingClass2 = MissingClass2; createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const innerSymbol = host.getClassSymbol(innerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!; @@ -2260,7 +2263,8 @@ exports.MissingClass2 = MissingClass2; const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2277,7 +2281,8 @@ exports.MissingClass2 = MissingClass2; const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2345,7 +2350,8 @@ exports.MissingClass2 = MissingClass2; createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; expect(host.isClass(innerNode)).toBe(true); }); 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 2319a5a8d2..8ef4acaabb 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -133,6 +133,14 @@ runInEachFileSystem(() => { class NoDecoratorConstructorClass { constructor(foo) {} } + let SimpleWrappedClass = /** @class */ (() => { + class SimpleWrappedClassInner {} + return SimpleWrappedClassInner; + })(); + let AliasedWrappedClass = /** @class */ (() => { + let AliasedWrappedClassAdjacent = class AliasedWrappedClassInner {}; + return SimpleWrappedClassAdjacent; + })(); `, }; @@ -166,6 +174,11 @@ runInEachFileSystem(() => { ]; return AliasedWrappedClass; })(); + let DecoratedWrappedClass = /** @class */ (() => { + let DecoratedWrappedClass = class DecoratedWrappedClass {} + // ... add decorations ... + return DecoratedWrappedClass; + })(); let usageOfWrappedClass = AliasedWrappedClass_1; `, }; @@ -534,7 +547,27 @@ runInEachFileSystem(() => { { type: Directive, args: [{ selector: '[b]' }] } ]; class C {} - export { A, x, C }; + var AliasedClass_1; + let AliasedClass = AliasedClass_1 = class AliasedClass {} + AliasedClass.decorators = [ + { type: Directive, args: [{ selector: '[aliased]' },] } + ]; + let Wrapped1 = /** @class */ (() => { + let Wrapped1 = class Wrapped1 { + }; + Wrapped1 = __decorate([ + Directive({selector: '[wrapped-1]'}) + ], Wrapped1); + return Wrapped1; + })(); + let Wrapped2 = /** @class */ (() => { + class Wrapped2 {} + Wrapped2.decorators = [ + { type: Directive, args: [{ selector: '[wrapped-2]' },] } + ]; + return Wrapped2; + })(); + export { A, x, C, AliasedClass, Wrapped1, Wrapped2 }; ` }, { @@ -1605,6 +1638,30 @@ runInEachFileSystem(() => { .toBe(classDeclaration); }); + it('should return the correct declaration for an inner class identifier inside an IIFE', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + + const outerDeclaration = getDeclaration( + bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + const innerDeclaration = + ((((outerDeclaration.initializer as ts.CallExpression).expression as + ts.ParenthesizedExpression) + .expression as ts.ArrowFunction) + .body as ts.Block) + .statements[0] as ts.ClassDeclaration; + + const outerIdentifier = outerDeclaration.name as ts.Identifier; + const innerIdentifier = innerDeclaration.name as ts.Identifier; + + expect(host.getDeclarationOfIdentifier(outerIdentifier)!.node).toBe(outerDeclaration); + expect(host.getDeclarationOfIdentifier(innerIdentifier)!.node).toBe(outerDeclaration); + }); + it('should recognize enum declarations with string values', () => { const testFile: TestFile = { name: _('/node_modules/test-package/some/file.js'), @@ -1767,6 +1824,7 @@ runInEachFileSystem(() => { expect(classSymbol).toBeDefined(); expect(classSymbol!.declaration.valueDeclaration).toBe(node); expect(classSymbol!.implementation.valueDeclaration).toBe(node); + expect(classSymbol!.adjacent).toBeUndefined(); }); it('should return the class symbol for a class expression (outer variable declaration)', @@ -1784,6 +1842,7 @@ runInEachFileSystem(() => { expect(classSymbol).toBeDefined(); expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode); expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode); + expect(classSymbol!.adjacent).toBeUndefined(); }); it('should return the class symbol for a class expression (inner class expression)', () => { @@ -1798,6 +1857,7 @@ runInEachFileSystem(() => { expect(classSymbol).toBeDefined(); expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode); expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode); + expect(classSymbol!.adjacent).toBeUndefined(); }); it('should return the same class symbol (of the outer declaration) for outer and inner declarations', @@ -1837,6 +1897,7 @@ runInEachFileSystem(() => { return fail('Expected a named class declaration'); } expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); + expect(classSymbol.adjacent).toBeUndefined(); }); it('should return the class symbol for a wrapped class expression (inner class expression)', @@ -1861,6 +1922,7 @@ runInEachFileSystem(() => { return fail('Expected a named class declaration'); } expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); + expect(classSymbol.adjacent).toBeUndefined(); }); it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations', @@ -1881,6 +1943,87 @@ runInEachFileSystem(() => { expect(innerSymbol.implementation).toBe(outerSymbol.implementation); }); + it('should return the class symbol for a decorated 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, 'DecoratedWrappedClass', + isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(outerNode); + + if (classSymbol === undefined) { + return fail('Expected classSymbol to be defined'); + } + expect(classSymbol.name).toEqual('DecoratedWrappedClass'); + expect(classSymbol.declaration.valueDeclaration).toBe(outerNode); + + if (!ts.isClassExpression(classSymbol.implementation.valueDeclaration)) { + return fail('Expected a named class declaration'); + } + expect(classSymbol.implementation.valueDeclaration.name!.text) + .toBe('DecoratedWrappedClass'); + + if (classSymbol.adjacent === undefined || + !isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) { + return fail('Expected a named variable declaration for the adjacent symbol'); + } + expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass'); + }); + + it('should return the class symbol for a decorated 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, 'DecoratedWrappedClass', + isNamedVariableDeclaration); + const innerNode: ts.ClassExpression = + (outerNode as any) + .initializer.expression.expression.body.statements[0] + .declarationList.declarations[0] + .initializer; + const classSymbol = host.getClassSymbol(innerNode); + + if (classSymbol === undefined) { + return fail('Expected classSymbol to be defined'); + } + expect(classSymbol.name).toEqual('DecoratedWrappedClass'); + expect(classSymbol.declaration.valueDeclaration).toBe(outerNode); + expect(classSymbol.implementation.valueDeclaration).toBe(innerNode); + + if (classSymbol.adjacent === undefined || + !isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) { + return fail('Expected a named variable declaration for the adjacent symbol'); + } + expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass'); + }); + + it('should return the same class symbol (of the outer declaration) for decorated 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, 'DecoratedWrappedClass', + isNamedVariableDeclaration); + const innerNode: ts.ClassExpression = + (outerNode as any) + .initializer.expression.expression.body.statements[0] + .declarationList.declarations[0] + .initializer; + + 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); @@ -1892,7 +2035,7 @@ runInEachFileSystem(() => { expect(classSymbol).toBeUndefined(); }); - it('should return undefined if variable declaration is not initialized using a class expression', + it('should return undefined if variable declaration is not initialized to a valid class definition', () => { const testFile = { name: _('/test.js'), @@ -2115,8 +2258,9 @@ runInEachFileSystem(() => { const secondaryFile = getSourceFileOrError(bundle.program, DECORATED_FILES[1].name); const classSymbolsPrimary = host.findClassSymbols(primaryFile); - expect(classSymbolsPrimary.length).toEqual(3); - expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B', 'C']); + expect(classSymbolsPrimary.map(c => c.name)).toEqual([ + 'A', 'B', 'C', 'AliasedClass', 'Wrapped1', 'Wrapped2' + ]); const classSymbolsSecondary = host.findClassSymbols(secondaryFile); expect(classSymbolsSecondary.length).toEqual(1); @@ -2134,10 +2278,14 @@ runInEachFileSystem(() => { const classSymbolsPrimary = host.findClassSymbols(primaryFile); const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); - expect(classDecoratorsPrimary.length).toEqual(3); + + expect(classDecoratorsPrimary.length).toEqual(6); expect(classDecoratorsPrimary[0]!.map(d => d.name)).toEqual(['Directive']); expect(classDecoratorsPrimary[1]!.map(d => d.name)).toEqual(['Directive']); expect(classDecoratorsPrimary[2]).toBe(null); + expect(classDecoratorsPrimary[3]!.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[4]!.map(d => d.name)).toEqual(['Directive']); + expect(classDecoratorsPrimary[5]!.map(d => d.name)).toEqual(['Directive']); const classSymbolsSecondary = host.findClassSymbols(secondaryFile); const classDecoratorsSecondary = @@ -2339,7 +2487,7 @@ runInEachFileSystem(() => { }); describe('getInternalNameOfClass()', () => { - it('should return the name of the class (there is no separate inner class in ES2015)', () => { + it('should return the name of the class (if there is no separate inner class)', () => { loadTestFiles([SIMPLE_CLASS_FILE]); const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); @@ -2347,10 +2495,32 @@ runInEachFileSystem(() => { bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass'); }); + + it('should return the name of the inner class (if there is an IIFE)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const node = getDeclaration( + bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(node).text).toEqual('SimpleWrappedClassInner'); + }); + + it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const node = getDeclaration( + bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass', + isNamedVariableDeclaration); + expect(host.getInternalNameOfClass(node).text).toEqual('AliasedWrappedClassInner'); + }); }); describe('getAdjacentNameOfClass()', () => { - it('should return the name of the class (there is no separate inner class in ES2015)', () => { + it('should return the name of the class (if there is no separate inner class)', () => { loadTestFiles([SIMPLE_CLASS_FILE]); const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); @@ -2358,6 +2528,28 @@ runInEachFileSystem(() => { bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass'); }); + + it('should return the name of the inner class (if there is an IIFE)', () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const node = getDeclaration( + bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass', + isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(node).text).toEqual('SimpleWrappedClassInner'); + }); + + it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)', + () => { + loadTestFiles([SIMPLE_CLASS_FILE]); + const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); + const host = + createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); + const node = getDeclaration( + bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass', + isNamedVariableDeclaration); + expect(host.getAdjacentNameOfClass(node).text).toEqual('AliasedWrappedClassAdjacent'); + }); }); describe('getEndOfClass()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts index 6ad45d394a..a3d08151db 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -12,7 +12,8 @@ import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/test import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers'; -import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; +import {getIifeBody} from '../../src/host/esm2015_host'; +import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils'; @@ -331,7 +332,8 @@ export { AliasedDirective$1 }; const classNode = getDeclaration( bundle.program, _('/some_minified_directive.js'), 'SomeDirective', isNamedVariableDeclaration); - const innerNode = getIifeBody(classNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(classNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(classNode); expect(classSymbol).toBeDefined(); 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 86a3372f69..fb7385bc0b 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -14,8 +14,8 @@ import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, Downleve import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; -import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; +import {Esm2015ReflectionHost, getIifeBody} from '../../src/host/esm2015_host'; +import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {BundleProgram} from '../../src/packages/bundle_program'; import {MockLogger} from '../helpers/mock_logger'; @@ -2283,7 +2283,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2297,7 +2298,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); @@ -2312,7 +2314,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const innerSymbol = host.getClassSymbol(innerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!; @@ -2327,7 +2330,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2343,7 +2347,8 @@ runInEachFileSystem(() => { const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2403,7 +2408,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; expect(host.isClass(innerNode)).toBe(true); }); 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 b5033c65dc..e5d0b2705d 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -14,7 +14,7 @@ import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Im import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; -import {getIifeBody} from '../../src/host/esm5_host'; +import {getIifeBody} from '../../src/host/esm2015_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {parseStatementForUmdModule, UmdReflectionHost} from '../../src/host/umd_host'; import {BundleProgram} from '../../src/packages/bundle_program'; @@ -2399,7 +2399,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2413,7 +2414,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(innerNode); expect(classSymbol).toBeDefined(); @@ -2428,7 +2430,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const innerSymbol = host.getClassSymbol(innerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!; @@ -2443,7 +2446,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2459,7 +2463,8 @@ runInEachFileSystem(() => { const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', isNamedVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; const classSymbol = host.getClassSymbol(outerNode); expect(classSymbol).toBeDefined(); @@ -2519,7 +2524,8 @@ runInEachFileSystem(() => { const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const outerNode = getDeclaration( bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; + const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block) + .statements.find(isNamedFunctionDeclaration)!; expect(host.isClass(innerNode)).toBe(true); });