diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index 8079e290c3..77490430aa 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -8,8 +8,8 @@ import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../ngtsc/reflection'; -import {getNameText} from '../utils'; +import {ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../ngtsc/reflection'; +import {getNameText, hasNameIdentifier} from '../utils'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; @@ -63,7 +63,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { * Find a symbol for a node that we think is a class. * * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE. - * So we need to dig around inside to get hold of the "class" symbol. + * So we might need to dig around inside to get hold of the "class" symbol. * * `node` might be one of: * - A class declaration (from a declaration file). @@ -87,33 +87,40 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { if (!innerClassIdentifier) return undefined; return this.checker.getSymbolAtLocation(innerClassIdentifier); - } else 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 undefined; - - // 2. IIFE function expression. - outerNode = outerNode.parent; - if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined; - - // 3. IIFE call expression. - outerNode = outerNode.parent; - if (!outerNode || !ts.isCallExpression(outerNode)) return undefined; - - // 4. Parenthesis around IIFE. - outerNode = outerNode.parent; - if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined; - - // 5. Outer variable declaration. - outerNode = outerNode.parent; - if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined; - - return this.getClassSymbol(outerNode); } - return undefined; + const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node); + + return outerClassNode && this.getClassSymbol(outerClassNode); + } + + /** + * Trace an identifier to its declaration, if possible. + * + * This method attempts to resolve the declaration of the given identifier, tracing back through + * imports and re-exports until the original declaration statement is found. A `Declaration` + * object is returned if the original declaration is found, or `null` is returned otherwise. + * + * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE. + * If we are looking for the declaration of the identifier of the inner function expression, we + * will get hold of the outer "class" variable declaration and return its identifier instead. See + * `getClassDeclarationFromInnerFunctionDeclaration()` for more info. + * + * @param id a TypeScript `ts.Identifier` to trace back to a declaration. + * + * @returns metadata about the `Declaration` if the original declaration is found, or `null` + * otherwise. + */ + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + // Get the identifier for the outer class node (if any). + const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent); + + if (outerClassNode && hasNameIdentifier(outerClassNode)) { + id = outerClassNode.name; + } + + // Resolve the identifier to a Symbol, and return the declaration of that. + return super.getDeclarationOfIdentifier(id); } /** @@ -347,6 +354,50 @@ 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): ts.VariableDeclaration| + undefined { + 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 undefined; + + // 2. IIFE function expression. + outerNode = outerNode.parent; + if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined; + + // 3. IIFE call expression. + outerNode = outerNode.parent; + if (!outerNode || !ts.isCallExpression(outerNode)) return undefined; + + // 4. Parenthesis around IIFE. + outerNode = outerNode.parent; + if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined; + + // 5. Outer variable declaration. + outerNode = outerNode.parent; + if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined; + + return outerNode; + } + + return undefined; +} + function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined { if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) { return undefined; diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 24f6d813a3..8efc087d2a 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -1278,6 +1278,34 @@ describe('Esm5ReflectionHost', () => { expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + + it('should return the correct declaration for an inner function identifier inside an ES5 IIFE', + () => { + const superGetDeclarationOfIdentifierSpy = + spyOn(Esm2015ReflectionHost.prototype, 'getDeclarationOfIdentifier').and.callThrough(); + const program = makeTestProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(false, program.getTypeChecker()); + + const outerDeclaration = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerDeclaration = (((outerDeclaration.initializer as ts.ParenthesizedExpression) + .expression as ts.CallExpression) + .expression as ts.FunctionExpression) + .body.statements[0] as ts.FunctionDeclaration; + + const outerIdentifier = outerDeclaration.name as ts.Identifier; + const innerIdentifier = innerDeclaration.name as ts.Identifier; + + expect(host.getDeclarationOfIdentifier(outerIdentifier) !.node).toBe(outerDeclaration); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); + + superGetDeclarationOfIdentifierSpy.calls.reset(); + + expect(host.getDeclarationOfIdentifier(innerIdentifier) !.node).toBe(outerDeclaration); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledWith(outerIdentifier); + expect(superGetDeclarationOfIdentifierSpy).toHaveBeenCalledTimes(1); + }); }); describe('getExportsOfModule()', () => {