fix(ivy): return correct declaration for class indentifiers for ES5 in ngcc (#26947)
PR Close #26947
This commit is contained in:
parent
1699c88655
commit
9d3dae42e9
|
@ -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;
|
||||||
|
|
|
@ -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()', () => {
|
||||||
|
|
Loading…
Reference in New Issue