Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
147 changes: 131 additions & 16 deletions packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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`.
*
Expand All @@ -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.
Expand All @@ -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);
}

Expand All @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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}`);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -2132,36 +2205,59 @@ 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.
*
* @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 expression;
}

if (!ts.isClassExpression(expression) || !hasNameIdentifier(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;
}

return expression;
// 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[] {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
40 changes: 3 additions & 37 deletions packages/compiler-cli/ngcc/src/host/esm5_host.ts
Expand Up @@ -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';


/**
Expand Down Expand Up @@ -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 /////////////
Expand Down Expand Up @@ -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 {
Expand Down