fix(ivy): return correct declaration for class indentifiers for ES5 in ngcc (#26947)

PR Close #26947
This commit is contained in:
George Kalpakas 2018-12-06 23:46:41 +02:00 committed by Jason Aden
parent 1699c88655
commit 9d3dae42e9
2 changed files with 107 additions and 28 deletions

View File

@ -8,8 +8,8 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../ngtsc/reflection'; import {ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../ngtsc/reflection';
import {getNameText} from '../utils'; import {getNameText, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; 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. * 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. * 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: * `node` might be one of:
* - A class declaration (from a declaration file). * - A class declaration (from a declaration file).
@ -87,33 +87,40 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
if (!innerClassIdentifier) return undefined; if (!innerClassIdentifier) return undefined;
return this.checker.getSymbolAtLocation(innerClassIdentifier); 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; 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 { function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined {
if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) { if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) {
return undefined; return undefined;

View File

@ -1278,6 +1278,34 @@ describe('Esm5ReflectionHost', () => {
expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core'); 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()', () => { describe('getExportsOfModule()', () => {