fix(ngcc): support TS 3.9 wrapped ES2015 classes (#36884)

In TS 3.9 the compiler will start to wrap ES2015 classes in an IIFE to help with
tree-shaking when the class has "associated" statements.

E.g.

```ts
let PlatformLocation = /** @class */ (() => {
    ...
    class PlatformLocation {
    }
    ...
    return PlatformLocation;
})();
```

This commit updates `Esm2015ReflectionHost` to support this format.

PR Close #36884
This commit is contained in:
Pete Bacon Darwin 2020-05-01 14:33:31 +01:00 committed by Alex Rickabaugh
parent 58ea040570
commit db4c59dad9
3 changed files with 256 additions and 54 deletions

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
@ -108,12 +108,18 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Classes should have a `name` identifier, because they may need to be referenced in other parts
* of the program.
*
* In ES2015, a class may be declared using a variable declaration of the following structure:
* In ES2015, a class may be declared using a variable declaration of the following structures:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })()
* ```
*
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` node is returned as declaration of `MyClass`.
*
@ -130,12 +136,18 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
* In ES2015, a class may be declared using a variable declaration of the following structures:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })()
* ```
*
* This method extracts the `NgccClassSymbol` for `MyClass` when provided with the `var MyClass`
* declaration node. When the `class MyClass {}` node or any other node is given, this method will
* return undefined instead.
@ -145,8 +157,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* of a class.
*/
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
// Create a symbol without inner declaration if the declaration is a regular class declaration.
if (ts.isClassDeclaration(declaration) && hasNameIdentifier(declaration)) {
// Create a symbol without inner declaration if it is a regular "top level" class declaration.
if (isNamedClassDeclaration(declaration) && isTopLevel(declaration)) {
return this.createClassSymbol(declaration, null);
}
@ -163,12 +175,18 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
* In ES2015, a class may be declared using a variable declaration of the following structures:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })()
* ```
*
* This method extracts the `NgccClassSymbol` for `MyClass` when provided with the
* `class MyClass {}` declaration node. When the `var MyClass` node or any other node is given,
* this method will return undefined instead.
@ -178,11 +196,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* of a class.
*/
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
if (!ts.isClassExpression(declaration) || !hasNameIdentifier(declaration)) {
let outerDeclaration: ts.VariableDeclaration|undefined = undefined;
if (isNamedClassDeclaration(declaration) && !isTopLevel(declaration)) {
let node = declaration.parent;
while (node !== undefined && !ts.isVariableDeclaration(node)) {
node = node.parent;
}
outerDeclaration = node;
} else if (ts.isClassExpression(declaration) && hasNameIdentifier(declaration)) {
outerDeclaration = getVariableDeclarationOfDeclaration(declaration);
} else {
return undefined;
}
const outerDeclaration = getVariableDeclarationOfDeclaration(declaration);
if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) {
return undefined;
}
@ -745,13 +772,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/**
* Try to retrieve the symbol of a static property on a class.
*
* In some cases, a static property can either be set on the inner declaration inside the class'
* IIFE, or it can be set on the outer variable declaration. Therefore, the host checks both
* places, first looking up the property on the inner symbol, and if the property is not found it
* will fall back to looking up the property on the outer symbol.
*
* @param symbol the class whose property we are interested in.
* @param propertyName the name of static property.
* @returns the symbol if it is found or `undefined` if not.
*/
protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol
|undefined {
return symbol.declaration.exports && symbol.declaration.exports.get(propertyName);
return symbol.implementation.exports && symbol.implementation.exports.get(propertyName) ||
symbol.declaration.exports && symbol.declaration.exports.get(propertyName);
}
/**
@ -1560,7 +1594,14 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
return Array.from(classSymbol.declaration.valueDeclaration.getSourceFile().statements);
const classNode = classSymbol.implementation.valueDeclaration;
if (isTopLevel(classNode)) {
return this.getModuleStatements(classNode.getSourceFile());
} else if (ts.isBlock(classNode.parent)) {
return Array.from(classNode.parent.statements);
}
// We should never arrive here
throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`);
}
/**
@ -2048,6 +2089,38 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass
ts.isIdentifier(statement.expression.left);
}
/**
* Parse the `expression` that is believed to be an IIFE and return the AST node that corresponds to
* the body of the IIFE.
*
* The expression may be wrapped in parentheses, which are stripped off.
*
* If the IIFE is an arrow function then its body could be a `ts.Expression` rather than a
* `ts.FunctionBody`.
*
* @param expression the expression to parse.
* @returns the `ts.Expression` or `ts.FunctionBody` that holds the body of the IIFE or `undefined`
* if the `expression` did not have the correct shape.
*/
export function getIifeConciseBody(expression: ts.Expression): ts.ConciseBody|undefined {
const call = stripParentheses(expression);
if (!ts.isCallExpression(call)) {
return undefined;
}
const fn = stripParentheses(call.expression);
if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) {
return undefined;
}
return fn.body;
}
/**
* Returns true if the `node` is an assignment of the form `a = b`.
*
* @param node The AST node to check.
*/
export function isAssignment(node: ts.Node): node is ts.AssignmentExpression<ts.EqualsToken> {
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken;
}
@ -2132,12 +2205,18 @@ function getCalleeName(call: ts.CallExpression): string|null {
///////////// Internal Helpers /////////////
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
* In ES2015, a class may be declared using a variable declaration of the following structures:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })()
* ```
*
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` expression is returned as declaration of `var MyClass`. If the variable
* is not initialized using a class expression, null is returned.
@ -2145,23 +2224,40 @@ function getCalleeName(call: ts.CallExpression): string|null {
* @param node the node that represents the class whose declaration we are finding.
* @returns the declaration of the class or `null` if it is not a "class".
*/
function getInnerClassDeclaration(node: ts.Node): ClassDeclaration<ts.ClassExpression>|null {
function getInnerClassDeclaration(node: ts.Node):
ClassDeclaration<ts.ClassExpression|ts.ClassDeclaration>|null {
if (!ts.isVariableDeclaration(node) || node.initializer === undefined) {
return null;
}
// Recognize a variable declaration of the form `var MyClass = class MyClass {}` or
// `var MyClass = MyClass_1 = class MyClass {};`
let expression = node.initializer;
while (isAssignment(expression)) {
expression = expression.right;
}
if (!ts.isClassExpression(expression) || !hasNameIdentifier(expression)) {
return null;
if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) {
return expression;
}
return expression;
// Try to parse out a class declaration wrapped in an IIFE (as generated by TS 3.9)
// e.g.
// /* @class */ = (() => {
// class MyClass {}
// ...
// return MyClass;
// })();
const iifeBody = getIifeConciseBody(expression);
if (iifeBody === undefined) {
return null;
}
// Extract the class declaration from inside the IIFE.
const innerDeclaration = ts.isBlock(iifeBody) ?
iifeBody.statements.find(ts.isClassDeclaration) :
ts.isClassExpression(iifeBody) ? iifeBody : undefined;
if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) {
return null;
}
return innerDeclaration;
}
function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] {
@ -2208,6 +2304,16 @@ function isClassMemberType(node: ts.Declaration): node is ts.ClassElement|
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* var MyClass = MyClass_1 = (() => {
* class MyClass {}
* ...
* return MyClass;
* })()
```
*
* and the provided declaration being `class MyClass {}`, this will return the `var MyClass`
* declaration.
*
@ -2301,3 +2407,12 @@ function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
return bundle.program.getSourceFiles().filter(
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
}
function isTopLevel(node: ts.Node): boolean {
while (node = node.parent) {
if (ts.isBlock(node)) {
return false;
}
}
return true;
}

View File

@ -11,9 +11,8 @@ import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, isNamedVariableDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host';
import {Esm2015ReflectionHost, getIifeConciseBody, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host';
import {NgccClassSymbol} from './ngcc_host';
import {stripParentheses} from './utils';
/**
@ -489,30 +488,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
/**
* Try to retrieve the symbol of a static property on a class.
*
* In ES5, a static property can either be set on the inner function declaration inside the class'
* IIFE, or it can be set on the outer variable declaration. Therefore, the ES5 host checks both
* places, first looking up the property on the inner symbol, and if the property is not found it
* will fall back to looking up the property on the outer symbol.
*
* @param symbol the class whose property we are interested in.
* @param propertyName the name of static property.
* @returns the symbol if it is found or `undefined` if not.
*/
protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol
|undefined {
// First lets see if the static property can be resolved from the inner class symbol.
const prop = symbol.implementation.exports && symbol.implementation.exports.get(propertyName);
if (prop !== undefined) {
return prop;
}
// Otherwise, lookup the static properties on the outer class symbol.
return symbol.declaration.exports && symbol.declaration.exports.get(propertyName);
}
}
///////////// Internal Helpers /////////////
@ -631,17 +606,8 @@ export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
parenthesizedCall = parenthesizedCall.right;
}
const call = stripParentheses(parenthesizedCall);
if (!ts.isCallExpression(call)) {
return undefined;
}
const fn = stripParentheses(call.expression);
if (!ts.isFunctionExpression(fn)) {
return undefined;
}
return fn.body;
const body = getIifeConciseBody(parenthesizedCall);
return body !== undefined && ts.isBlock(body) ? body : undefined;
}
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {

View File

@ -30,6 +30,7 @@ runInEachFileSystem(() => {
let ACCESSORS_FILE: TestFile;
let SIMPLE_CLASS_FILE: TestFile;
let CLASS_EXPRESSION_FILE: TestFile;
let WRAPPED_CLASS_EXPRESSION_FILE: TestFile;
let FOO_FUNCTION_FILE: TestFile;
let INVALID_DECORATORS_FILE: TestFile;
let INVALID_DECORATOR_ARGS_FILE: TestFile;
@ -150,6 +151,26 @@ runInEachFileSystem(() => {
`,
};
WRAPPED_CLASS_EXPRESSION_FILE = {
name: _('/wrapped_class_expression.js'),
contents: `
import {Directive} from '@angular/core';
var AliasedWrappedClass_1;
let SimpleWrappedClass = /** @class */ (() => {
class SimpleWrappedClass {}
return SimpleWrappedClass;
})();
let AliasedWrappedClass = AliasedWrappedClass_1 = /** @class */ (() => {
class AliasedWrappedClass {}
AliasedWrappedClass.decorators = [
{ type: Directive, args: [{ selector: '[someDirective]' },] }
];
return AliasedWrappedClass;
})();
let usageOfWrappedClass = AliasedWrappedClass_1;
`,
};
FOO_FUNCTION_FILE = {
name: _('/foo_function.js'),
contents: `
@ -762,6 +783,26 @@ runInEachFileSystem(() => {
]);
});
it('should find the decorators on an aliased wrapped class', () => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const classNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode)!;
expect(decorators).not.toBe(null!);
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args!.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should return null if the symbol is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
@ -1620,6 +1661,22 @@ runInEachFileSystem(() => {
.toBe(classDeclaration);
});
it('should return the original declaration of an aliased class', () => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const classDeclaration = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass',
ts.isVariableDeclaration);
const usageOfWrappedClass = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'usageOfWrappedClass',
ts.isVariableDeclaration);
const aliasedClassIdentifier = usageOfWrappedClass.initializer as ts.Identifier;
expect(aliasedClassIdentifier.text).toBe('AliasedWrappedClass_1');
expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier)!.node)
.toBe(classDeclaration);
});
it('should recognize enum declarations with string values', () => {
const testFile: TestFile = {
name: _('/node_modules/test-package/some/file.js'),
@ -1832,6 +1889,70 @@ runInEachFileSystem(() => {
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return the class symbol for a wrapped class expression (outer variable declaration)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(outerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('SimpleWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
});
it('should return the class symbol for a wrapped class expression (inner class expression)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block)
.statements[0];
const classSymbol = host.getClassSymbol(innerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('SimpleWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
});
it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block)
.statements[0];
const innerSymbol = host.getClassSymbol(innerNode)!;
const outerSymbol = host.getClassSymbol(outerNode)!;
expect(innerSymbol.declaration).toBe(outerSymbol.declaration);
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return undefined if node is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);