fix(ngcc): ensure reflection hosts can handle TS 3.9 IIFE wrapped classes (#36989)

In TS 3.9, ES2015 output can contain ES classes that are wrapped in an
IIFE. So now ES2015 class declarations can look like one of:

```
class OuterClass1 {}
```

```
let OuterClass = class InnerClass {};
```

```
var AliasClass;
let OuterClass = AliasClass = class InnerClass {};
```

```
let OuterClass = (() => class InnerClass {}};
```

```
var AliasClass;
let OuterClass = AliasClass = (() => class InnerClass {})();
```

```
let OuterClass = (() => {
  let AdjacentClass = class InnerClass {};
  // ... static properties or decorators attached to `AdjacentClass`
  return AdjacentClass;
})();
```

```
var AliasClass;
let OuterClass = AliasClass = (() => {
  let AdjacentClass = class InnerClass {};
  // ... static properties or decorators attached to `AdjacentClass`
  return AdjacentClass;
})();
```

The `Esm5ReflectionHost` already handles slightly different IIFE wrappers
around function-based classes. This can be substantially reused when
fixing `Esm2015ReflectionHost`, since there is a lot of commonality
between the two.

This commit moves code from the `Esm5ReflectionHost` into the `Esm2015ReflectionHost`
and looks to share as much as possible between the two hosts.

PR Close #36989
This commit is contained in:
Pete Bacon Darwin 2020-05-12 08:20:00 +01:00 committed by Kara Erickson
parent a2b8dc1cfb
commit d7440c452a
8 changed files with 792 additions and 467 deletions

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util'; import {isWithinPackage} from '../analysis/util';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program'; import {BundleProgram} from '../packages/bundle_program';
@ -135,122 +135,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return this.getClassSymbolFromInnerDeclaration(declaration); return this.getClassSymbolFromInnerDeclaration(declaration);
} }
/**
* 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.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it does not represent an outer declaration
* of a class.
*/
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
// 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);
}
// Otherwise, the declaration may be a variable declaration, in which case it must be
// initialized using a class expression as inner declaration.
if (ts.isVariableDeclaration(declaration) && hasNameIdentifier(declaration)) {
const innerDeclaration = getInnerClassDeclaration(declaration);
if (innerDeclaration !== null) {
return this.createClassSymbol(declaration, innerDeclaration);
}
}
return undefined;
}
/**
* 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.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it does not represent an inner declaration
* of a class.
*/
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
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;
}
if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) {
return undefined;
}
return this.createClassSymbol(outerDeclaration, declaration);
}
/**
* Creates an `NgccClassSymbol` from an outer and inner declaration. If a class only has an outer
* declaration, the "implementation" symbol of the created `NgccClassSymbol` will be set equal to
* the "declaration" symbol.
*
* @param outerDeclaration The outer declaration node of the class.
* @param innerDeclaration The inner declaration node of the class, or undefined if no inner
* declaration is present.
* @returns the `NgccClassSymbol` representing the class, or undefined if a `ts.Symbol` for any of
* the declarations could not be resolved.
*/
protected createClassSymbol(
outerDeclaration: ClassDeclaration, innerDeclaration: ClassDeclaration|null): NgccClassSymbol
|undefined {
const declarationSymbol =
this.checker.getSymbolAtLocation(outerDeclaration.name) as ClassSymbol | undefined;
if (declarationSymbol === undefined) {
return undefined;
}
const implementationSymbol = innerDeclaration !== null ?
this.checker.getSymbolAtLocation(innerDeclaration.name) :
declarationSymbol;
if (implementationSymbol === undefined) {
return undefined;
}
return {
name: declarationSymbol.name,
declaration: declarationSymbol,
implementation: implementationSymbol,
};
}
/** /**
* Examine a declaration (for example, of a class or function) and return metadata about any * Examine a declaration (for example, of a class or function) and return metadata about any
* decorators present on the declaration. * decorators present on the declaration.
@ -319,17 +203,60 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
} }
getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
// First try getting the base class from the "outer" declaration // First try getting the base class from an ES2015 class declaration
const superBaseClassIdentifier = super.getBaseClassExpression(clazz); const superBaseClassIdentifier = super.getBaseClassExpression(clazz);
if (superBaseClassIdentifier) { if (superBaseClassIdentifier) {
return superBaseClassIdentifier; return superBaseClassIdentifier;
} }
// That didn't work so now try getting it from the "inner" declaration. // That didn't work so now try getting it from the "inner" declaration.
const innerClassDeclaration = getInnerClassDeclaration(clazz); const classSymbol = this.getClassSymbol(clazz);
if (innerClassDeclaration === null) { if (classSymbol === undefined ||
!isNamedDeclaration(classSymbol.implementation.valueDeclaration)) {
return null; return null;
} }
return super.getBaseClassExpression(innerClassDeclaration); return super.getBaseClassExpression(classSymbol.implementation.valueDeclaration);
}
getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier {
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
throw new Error(`getInternalNameOfClass() called on a non-class: expected ${
clazz.name.text} to be a class declaration.`);
}
return this.getNameFromClassSymbolDeclaration(
classSymbol, classSymbol.implementation.valueDeclaration);
}
getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier {
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
throw new Error(`getAdjacentNameOfClass() called on a non-class: expected ${
clazz.name.text} to be a class declaration.`);
}
if (classSymbol.adjacent !== undefined) {
return this.getNameFromClassSymbolDeclaration(
classSymbol, classSymbol.adjacent.valueDeclaration);
} else {
return this.getNameFromClassSymbolDeclaration(
classSymbol, classSymbol.implementation.valueDeclaration);
}
}
private getNameFromClassSymbolDeclaration(
classSymbol: NgccClassSymbol, declaration: ts.Declaration): ts.Identifier {
if (declaration === undefined) {
throw new Error(
`getInternalNameOfClass() called on a class with an undefined internal declaration. External class name: ${
classSymbol.name}; internal class name: ${classSymbol.implementation.name}.`);
}
if (!isNamedDeclaration(declaration)) {
throw new Error(
`getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${
declaration.getText()}`);
}
return declaration.name;
} }
/** /**
@ -368,22 +295,30 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return superDeclaration; return superDeclaration;
} }
const outerClassNode = getClassDeclarationFromInnerDeclaration(superDeclaration.node);
const declaration = outerClassNode !== null ?
this.getDeclarationOfIdentifier(outerClassNode.name) :
superDeclaration;
if (declaration === null || declaration.node === null || declaration.known !== null) {
return declaration;
}
// The identifier may have been of an additional class assignment such as `MyClass_1` that was // The identifier may have been of an additional class assignment such as `MyClass_1` that was
// present as alias for `MyClass`. If so, resolve such aliases to their original declaration. // present as alias for `MyClass`. If so, resolve such aliases to their original declaration.
const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node); const aliasedIdentifier = this.resolveAliasedClassIdentifier(declaration.node);
if (aliasedIdentifier !== null) { if (aliasedIdentifier !== null) {
return this.getDeclarationOfIdentifier(aliasedIdentifier); return this.getDeclarationOfIdentifier(aliasedIdentifier);
} }
// Variable declarations may represent an enum declaration, so attempt to resolve its members. // Variable declarations may represent an enum declaration, so attempt to resolve its members.
if (ts.isVariableDeclaration(superDeclaration.node)) { if (ts.isVariableDeclaration(declaration.node)) {
const enumMembers = this.resolveEnumMembers(superDeclaration.node); const enumMembers = this.resolveEnumMembers(declaration.node);
if (enumMembers !== null) { if (enumMembers !== null) {
superDeclaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; declaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers};
} }
} }
return superDeclaration; return declaration;
} }
/** /**
@ -555,31 +490,47 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
} }
getEndOfClass(classSymbol: NgccClassSymbol): ts.Node { getEndOfClass(classSymbol: NgccClassSymbol): ts.Node {
let last: ts.Node = classSymbol.declaration.valueDeclaration; const implementation = classSymbol.implementation;
let last: ts.Node = implementation.valueDeclaration;
const implementationStatement = getContainingStatement(last);
if (implementationStatement === null) return last;
// If there are static members on this class then find the last one const container = implementationStatement.parent;
if (classSymbol.declaration.exports !== undefined) { if (ts.isBlock(container)) {
classSymbol.declaration.exports.forEach(exportSymbol => { // Assume that the implementation is inside an IIFE
if (exportSymbol.valueDeclaration === undefined) { const returnStatementIndex = container.statements.findIndex(ts.isReturnStatement);
return; if (returnStatementIndex === -1) {
} throw new Error(
const exportStatement = getContainingStatement(exportSymbol.valueDeclaration); `Compiled class wrapper IIFE does not have a return statement: ${classSymbol.name} in ${
if (exportStatement !== null && last.getEnd() < exportStatement.getEnd()) { classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`);
last = exportStatement; }
// Return the statement before the IIFE return statement
last = container.statements[returnStatementIndex - 1];
} else if (ts.isSourceFile(container)) {
// If there are static members on this class then find the last one
if (implementation.exports !== undefined) {
implementation.exports.forEach(exportSymbol => {
if (exportSymbol.valueDeclaration === undefined) {
return;
}
const exportStatement = getContainingStatement(exportSymbol.valueDeclaration);
if (exportStatement !== null && last.getEnd() < exportStatement.getEnd()) {
last = exportStatement;
}
});
}
// If there are helper calls for this class then find the last one
const helpers = this.getHelperCallsForClass(
classSymbol, ['__decorate', '__extends', '__param', '__metadata']);
helpers.forEach(helper => {
const helperStatement = getContainingStatement(helper);
if (helperStatement !== null && last.getEnd() < helperStatement.getEnd()) {
last = helperStatement;
} }
}); });
} }
// If there are helper calls for this class then find the last one
const helpers = this.getHelperCallsForClass(
classSymbol, ['__decorate', '__extends', '__param', '__metadata']);
helpers.forEach(helper => {
const helperStatement = getContainingStatement(helper);
if (helperStatement !== null && last.getEnd() < helperStatement.getEnd()) {
last = helperStatement;
}
});
return last; return last;
} }
@ -602,6 +553,186 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
/**
* A class may be declared as a top level class declaration:
*
* ```
* class OuterClass { ... }
* ```
*
* or in a variable declaration to a class expression:
*
* ```
* var OuterClass = ClassAlias = class InnerClass {};
* ```
*
* or in a variable declaration to an IIFE containing a class declaration
*
* ```
* var OuterClass = ClassAlias = (() => {
* class InnerClass {}
* ...
* return InnerClass;
* })()
* ```
*
* or in a variable declaration to an IIFE containing a function declaration
*
* ```
* var OuterClass = ClassAlias = (() => {
* function InnerClass() {}
* ...
* return InnerClass;
* })()
* ```
*
* This method returns an `NgccClassSymbol` when provided with one of these cases.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the class or `undefined` if `declaration` does not represent an outer
* declaration of a class.
*/
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
// Return a class symbol without an inner declaration if it is a regular "top level" class
if (isNamedClassDeclaration(declaration) && isTopLevel(declaration)) {
return this.createClassSymbol(declaration, null);
}
// Otherwise, an outer class declaration must be an initialized variable declaration:
if (!isInitializedVariableClassDeclaration(declaration)) {
return undefined;
}
const innerDeclaration = getInnerClassDeclaration(skipClassAliases(declaration));
if (innerDeclaration !== null) {
return this.createClassSymbol(declaration, innerDeclaration);
}
return undefined;
}
/**
* In ES2015, a class may be declared using a variable declaration of the following structures:
*
* ```
* let MyClass = MyClass_1 = class MyClass {};
* ```
*
* or
*
* ```
* let MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })()
* ```
*
* or
*
* ```
* let MyClass = MyClass_1 = (() => { let MyClass = 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.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it does not represent an inner declaration
* of a class.
*/
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
let outerDeclaration: ts.ClassDeclaration|ts.VariableDeclaration|undefined = undefined;
if (ts.isClassExpression(declaration) && hasNameIdentifier(declaration)) {
// Handle `let MyClass = MyClass_1 = class MyClass {};`
outerDeclaration = getFarLeftHandSideOfAssignment(declaration);
// Handle this being in an IIFE
if (outerDeclaration !== undefined && !isTopLevel(outerDeclaration)) {
outerDeclaration = getContainingVariableDeclaration(outerDeclaration);
}
} else if (isNamedClassDeclaration(declaration)) {
// Handle `class MyClass {}` statement
if (isTopLevel(declaration)) {
// At the top level
outerDeclaration = declaration;
} else {
// Or inside an IIFE
outerDeclaration = getContainingVariableDeclaration(declaration);
}
}
if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) {
return undefined;
}
return this.createClassSymbol(outerDeclaration, declaration);
}
/**
* Creates an `NgccClassSymbol` from an outer and inner declaration. If a class only has an outer
* declaration, the "implementation" symbol of the created `NgccClassSymbol` will be set equal to
* the "declaration" symbol.
*
* @param outerDeclaration The outer declaration node of the class.
* @param innerDeclaration The inner declaration node of the class, or undefined if no inner
* declaration is present.
* @returns the `NgccClassSymbol` representing the class, or undefined if a `ts.Symbol` for any of
* the declarations could not be resolved.
*/
protected createClassSymbol(outerDeclaration: ClassDeclaration, innerDeclaration: ts.Node|null):
NgccClassSymbol|undefined {
const declarationSymbol =
this.checker.getSymbolAtLocation(outerDeclaration.name) as ClassSymbol | undefined;
if (declarationSymbol === undefined) {
return undefined;
}
let implementationSymbol = declarationSymbol;
if (innerDeclaration !== null && isNamedDeclaration(innerDeclaration)) {
implementationSymbol = this.checker.getSymbolAtLocation(innerDeclaration.name) as ClassSymbol;
}
if (implementationSymbol === undefined) {
return undefined;
}
const classSymbol: NgccClassSymbol = {
name: declarationSymbol.name,
declaration: declarationSymbol,
implementation: implementationSymbol,
};
let adjacent = this.getAdjacentSymbol(declarationSymbol, implementationSymbol);
if (adjacent !== null) {
classSymbol.adjacent = adjacent;
}
return classSymbol;
}
private getAdjacentSymbol(declarationSymbol: ClassSymbol, implementationSymbol: ClassSymbol):
ClassSymbol|undefined {
if (declarationSymbol === implementationSymbol) {
return undefined;
}
const innerDeclaration = implementationSymbol.valueDeclaration;
if (!ts.isClassExpression(innerDeclaration) && !ts.isFunctionExpression(innerDeclaration)) {
return undefined;
}
// Deal with the inner class looking like this inside an IIFE:
// `let MyClass = class MyClass {};` or `var MyClass = function MyClass() {};`
const adjacentDeclaration = getFarLeftHandSideOfAssignment(innerDeclaration);
if (adjacentDeclaration === undefined || !isNamedVariableDeclaration(adjacentDeclaration)) {
return undefined;
}
const adjacentSymbol =
this.checker.getSymbolAtLocation(adjacentDeclaration.name) as ClassSymbol;
if (adjacentSymbol === declarationSymbol || adjacentSymbol === implementationSymbol) {
return undefined;
}
return adjacentSymbol;
}
/** /**
* Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known * Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known
* declaration. * declaration.
@ -611,7 +742,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
const declaration = super.getDeclarationOfSymbol(symbol, originalId); const declaration = super.getDeclarationOfSymbol(symbol, originalId);
if (declaration === null) { if (declaration === null) {
return null; return null;
} }
return this.detectKnownDeclaration(declaration); return this.detectKnownDeclaration(declaration);
} }
@ -892,6 +1023,33 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
} }
} }
// If this class was declared as a VariableDeclaration inside an IIFE, then it may have static
// properties attached to the variable rather than the class itself.
//
// For example:
// ```
// let OuterClass = (() => {
// let AdjacentClass = class InternalClass {
// // no static properties here!
// }
// AdjacentClass.staticProperty = ...;
// })();
// ```
if (symbol.adjacent !== undefined) {
if (ts.isVariableDeclaration(symbol.adjacent.valueDeclaration)) {
if (symbol.adjacent.exports !== undefined) {
symbol.adjacent.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators, true);
if (reflectedMembers) {
decoratorsMap.delete(key as string);
members.push(...reflectedMembers);
}
});
}
}
}
// Deal with any decorated properties that were not initialized in the class // Deal with any decorated properties that were not initialized in the class
decoratorsMap.forEach((value, key) => { decoratorsMap.forEach((value, key) => {
members.push({ members.push({
@ -991,15 +1149,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
const outerDeclaration = classSymbol.declaration.valueDeclaration; const outerDeclaration = classSymbol.declaration.valueDeclaration;
const innerDeclaration = classSymbol.implementation.valueDeclaration; const innerDeclaration = classSymbol.implementation.valueDeclaration;
const adjacentDeclaration =
this.getAdjacentNameOfClass((classSymbol.declaration.valueDeclaration)).parent;
const matchesClass = (identifier: ts.Identifier) => { const matchesClass = (identifier: ts.Identifier) => {
const decl = this.getDeclarationOfIdentifier(identifier); const decl = this.getDeclarationOfIdentifier(identifier);
if (decl === null) { return decl !== null &&
return false; (decl.node === adjacentDeclaration || decl.node === outerDeclaration ||
} decl.node === innerDeclaration);
// The identifier corresponds with the class if its declaration is either the outer or inner
// declaration.
return decl.node === outerDeclaration || decl.node === innerDeclaration;
}; };
for (const helperCall of helperCalls) { for (const helperCall of helperCalls) {
@ -1545,8 +1701,10 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
const classNode = classSymbol.implementation.valueDeclaration; const classNode = classSymbol.implementation.valueDeclaration;
if (isTopLevel(classNode)) { if (isTopLevel(classNode)) {
return this.getModuleStatements(classNode.getSourceFile()); return this.getModuleStatements(classNode.getSourceFile());
} else if (ts.isBlock(classNode.parent)) { }
return Array.from(classNode.parent.statements); const statement = getContainingStatement(classNode);
if (ts.isBlock(statement.parent)) {
return Array.from(statement.parent.statements);
} }
// We should never arrive here // We should never arrive here
throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`); throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`);
@ -1991,7 +2149,7 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass
* @returns the `ts.Expression` or `ts.FunctionBody` that holds the body of the IIFE or `undefined` * @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. * if the `expression` did not have the correct shape.
*/ */
export function getIifeConciseBody(expression: ts.Expression): ts.ConciseBody|undefined { export function getIifeBody(expression: ts.Expression): ts.ConciseBody|undefined {
const call = stripParentheses(expression); const call = stripParentheses(expression);
if (!ts.isCallExpression(call)) { if (!ts.isCallExpression(call)) {
return undefined; return undefined;
@ -2093,60 +2251,107 @@ function getCalleeName(call: ts.CallExpression): string|null {
///////////// Internal Helpers ///////////// ///////////// Internal Helpers /////////////
type InitializedVariableClassDeclaration =
ClassDeclaration<ts.VariableDeclaration>&{initializer: ts.Expression};
function isInitializedVariableClassDeclaration(node: ts.Node):
node is InitializedVariableClassDeclaration {
return isNamedVariableDeclaration(node) && node.initializer !== undefined;
}
/** /**
* In ES2015, a class may be declared using a variable declaration of the following structures: * Handle a variable declaration of the form
* *
* ``` * ```
* var MyClass = MyClass_1 = class MyClass {}; * var MyClass = alias1 = alias2 = <<declaration>>
* ``` * ```
* *
* or * @node the LHS of a variable declaration.
* * @returns the original AST node or the RHS of a series of assignments in a variable
* ``` * declaration.
* 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): export function skipClassAliases(node: InitializedVariableClassDeclaration): ts.Expression {
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; let expression = node.initializer;
while (isAssignment(expression)) { while (isAssignment(expression)) {
expression = expression.right; expression = expression.right;
} }
return expression;
}
/**
* This expression could either be a class expression
*
* ```
* class MyClass {};
* ```
*
* or an IIFE wrapped class expression
*
* ```
* (() => {
* class MyClass {}
* ...
* return MyClass;
* })()
* ```
*
* or an IIFE wrapped aliased class expression
*
* ```
* (() => {
* let MyClass = class MyClass {}
* ...
* return MyClass;
* })()
* ```
*
* or an IFFE wrapped ES5 class function
*
* ```
* (function () {
* function MyClass() {}
* ...
* return MyClass
* })()
* ```
*
* @param expression 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(expression: ts.Expression):
ClassDeclaration<ts.ClassExpression|ts.ClassDeclaration|ts.FunctionDeclaration>|null {
if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) { 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) const iifeBody = getIifeBody(expression);
// e.g.
// /* @class */ = (() => {
// class MyClass {}
// ...
// return MyClass;
// })();
const iifeBody = getIifeConciseBody(expression);
if (iifeBody === undefined) { if (iifeBody === undefined) {
return null; return null;
} }
// Extract the class declaration from inside the IIFE.
const innerDeclaration = ts.isBlock(iifeBody) ? if (!ts.isBlock(iifeBody)) {
iifeBody.statements.find(ts.isClassDeclaration) : // Handle the fat arrow expression case: `() => ClassExpression`
ts.isClassExpression(iifeBody) ? iifeBody : undefined; return ts.isClassExpression(iifeBody) && isNamedDeclaration(iifeBody) ? iifeBody : null;
if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) { } else {
return null; // Handle the case of a normal or fat-arrow function with a body.
// Return the first ClassDeclaration/VariableDeclaration inside the body
for (const statement of iifeBody.statements) {
if (isNamedClassDeclaration(statement) || isNamedFunctionDeclaration(statement)) {
return statement;
}
if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations) {
if (isInitializedVariableClassDeclaration(declaration)) {
const expression = skipClassAliases(declaration);
if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) {
return expression;
}
}
}
}
}
} }
return innerDeclaration;
return null;
} }
function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] {
@ -2170,8 +2375,7 @@ function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression&
node.left.expression.kind === ts.SyntaxKind.ThisKeyword; node.left.expression.kind === ts.SyntaxKind.ThisKeyword;
} }
function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration& function isNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration&{name: ts.Identifier} {
{name: ts.Identifier} {
const anyNode: any = node; const anyNode: any = node;
return !!anyNode.name && ts.isIdentifier(anyNode.name); return !!anyNode.name && ts.isIdentifier(anyNode.name);
} }
@ -2209,7 +2413,7 @@ function isClassMemberType(node: ts.Declaration): node is ts.ClassElement|
* @param declaration The declaration for which any variable declaration should be obtained. * @param declaration The declaration for which any variable declaration should be obtained.
* @returns the outer variable declaration if found, undefined otherwise. * @returns the outer variable declaration if found, undefined otherwise.
*/ */
function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.VariableDeclaration| function getFarLeftHandSideOfAssignment(declaration: ts.Declaration): ts.VariableDeclaration|
undefined { undefined {
let node = declaration.parent; let node = declaration.parent;
@ -2221,6 +2425,18 @@ function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.Va
return ts.isVariableDeclaration(node) ? node : undefined; return ts.isVariableDeclaration(node) ? node : undefined;
} }
function getContainingVariableDeclaration(node: ts.Node): ClassDeclaration<ts.VariableDeclaration>|
undefined {
node = node.parent;
while (node !== undefined) {
if (isNamedVariableDeclaration(node)) {
return node;
}
node = node.parent;
}
return undefined;
}
/** /**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit, * A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used. * in the case no user-defined constructor exists and e.g. property initializers are used.
@ -2273,14 +2489,14 @@ function isSynthesizedSuperCall(expression: ts.Expression): boolean {
* Find the statement that contains the given node * Find the statement that contains the given node
* @param node a node whose containing statement we wish to find * @param node a node whose containing statement we wish to find
*/ */
function getContainingStatement(node: ts.Node): ts.ExpressionStatement|null { function getContainingStatement(node: ts.Node): ts.Statement {
while (node) { while (node.parent) {
if (ts.isExpressionStatement(node)) { if (ts.isBlock(node.parent) || ts.isSourceFile(node.parent)) {
break; break;
} }
node = node.parent; node = node.parent;
} }
return node || null; return node as ts.Statement;
} }
function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile { function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
@ -2305,3 +2521,52 @@ function isTopLevel(node: ts.Node): boolean {
} }
return true; return true;
} }
/**
* 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".
*/
export function getClassDeclarationFromInnerDeclaration(node: ts.Node):
ClassDeclaration<ts.VariableDeclaration>|null {
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
// It might be the function expression inside the IIFE. We need to go 5 levels up...
// - IIFE body.
let outerNode = node.parent;
if (!outerNode || !ts.isBlock(outerNode)) return null;
// - IIFE function expression.
outerNode = outerNode.parent;
if (!outerNode || (!ts.isFunctionExpression(outerNode) && !ts.isArrowFunction(outerNode))) {
return null;
}
outerNode = outerNode.parent;
// - Parenthesis inside IIFE.
if (outerNode && ts.isParenthesizedExpression(outerNode)) outerNode = outerNode.parent;
// - IIFE call expression.
if (!outerNode || !ts.isCallExpression(outerNode)) return null;
outerNode = outerNode.parent;
// - Parenthesis around IIFE.
if (outerNode && ts.isParenthesizedExpression(outerNode)) outerNode = outerNode.parent;
// - Outer variable declaration.
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return null;
// Finally, ensure that the variable declaration has a `name` identifier.
return hasNameIdentifier(outerNode) ? outerNode : null;
}
return null;
}

View File

@ -8,10 +8,10 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, isNamedVariableDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils'; import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, getIifeConciseBody, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
import {NgccClassSymbol} from './ngcc_host'; import {NgccClassSymbol} from './ngcc_host';
@ -23,6 +23,7 @@ import {NgccClassSymbol} from './ngcc_host';
* function CommonModule() { * function CommonModule() {
* } * }
* CommonModule.decorators = [ ... ]; * CommonModule.decorators = [ ... ];
* return CommonModule;
* ``` * ```
* *
* * "Classes" are decorated if they have a static property called `decorators`. * * "Classes" are decorated if they have a static property called `decorators`.
@ -34,16 +35,13 @@ import {NgccClassSymbol} from './ngcc_host';
*/ */
export class Esm5ReflectionHost extends Esm2015ReflectionHost { export class Esm5ReflectionHost extends Esm2015ReflectionHost {
getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null {
const classSymbol = this.getClassSymbol(clazz); const superBaseClassExpression = super.getBaseClassExpression(clazz);
if (classSymbol === undefined) { if (superBaseClassExpression !== null) {
return null; return superBaseClassExpression;
} }
const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration); const iife = getIifeFn(this.getClassSymbol(clazz));
if (!iifeBody) return null; if (iife === null) return null;
const iife = iifeBody.parent;
if (!iife || !ts.isFunctionExpression(iife)) return null;
if (iife.parameters.length !== 1 || !isSuperIdentifier(iife.parameters[0].name)) { if (iife.parameters.length !== 1 || !isSuperIdentifier(iife.parameters[0].name)) {
return null; return null;
@ -56,100 +54,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return iife.parent.arguments[0]; return iife.parent.arguments[0];
} }
getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier {
const innerClass = this.getInnerFunctionDeclarationFromClassDeclaration(clazz);
if (innerClass === undefined) {
throw new Error(`getInternalNameOfClass() called on a non-ES5 class: expected ${
clazz.name.text} to have an inner class declaration`);
}
if (innerClass.name === undefined) {
throw new Error(
`getInternalNameOfClass() called on a class with an anonymous inner declaration: expected a name on:\n${
innerClass.getText()}`);
}
return innerClass.name;
}
getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier {
return this.getInternalNameOfClass(clazz);
}
getEndOfClass(classSymbol: NgccClassSymbol): ts.Node {
const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration);
if (!iifeBody) {
throw new Error(`Compiled class declaration is not inside an IIFE: ${classSymbol.name} in ${
classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`);
}
const returnStatementIndex = iifeBody.statements.findIndex(ts.isReturnStatement);
if (returnStatementIndex === -1) {
throw new Error(
`Compiled class wrapper IIFE does not have a return statement: ${classSymbol.name} in ${
classSymbol.declaration.valueDeclaration.getSourceFile().fileName}`);
}
// Return the statement before the IIFE return statement
return iifeBody.statements[returnStatementIndex - 1];
}
/**
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
* whose value is assigned to a variable (which represents the class to the rest of the program).
* So we might need to dig around to get hold of the "class" declaration.
*
* This method extracts a `NgccClassSymbol` if `declaration` is the outer variable which is
* assigned the result of the IIFE. Otherwise, undefined is returned.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
const classSymbol = super.getClassSymbolFromOuterDeclaration(declaration);
if (classSymbol !== undefined) {
return classSymbol;
}
if (!isNamedVariableDeclaration(declaration)) {
return undefined;
}
const innerDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(declaration);
if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) {
return undefined;
}
return this.createClassSymbol(declaration, innerDeclaration);
}
/**
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
* whose value is assigned to a variable (which represents the class to the rest of the program).
* So we might need to dig around to get hold of the "class" declaration.
*
* This method extracts a `NgccClassSymbol` if `declaration` is the function declaration inside
* the IIFE. Otherwise, undefined is returned.
*
* @param declaration the declaration whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
if (classSymbol !== undefined) {
return classSymbol;
}
if (!ts.isFunctionDeclaration(declaration) || !hasNameIdentifier(declaration)) {
return undefined;
}
const outerDeclaration = getClassDeclarationFromInnerFunctionDeclaration(declaration);
if (outerDeclaration === null || !hasNameIdentifier(outerDeclaration)) {
return undefined;
}
return this.createClassSymbol(outerDeclaration, declaration);
}
/** /**
* Trace an identifier to its declaration, if possible. * Trace an identifier to its declaration, if possible.
* *
@ -168,9 +72,9 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* otherwise. * otherwise.
*/ */
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
const superDeclaration = super.getDeclarationOfIdentifier(id); const declaration = super.getDeclarationOfIdentifier(id);
if (superDeclaration === null) { if (declaration === null) {
const nonEmittedNorImportedTsHelperDeclaration = getTsHelperFnFromIdentifier(id); const nonEmittedNorImportedTsHelperDeclaration = getTsHelperFnFromIdentifier(id);
if (nonEmittedNorImportedTsHelperDeclaration !== null) { if (nonEmittedNorImportedTsHelperDeclaration !== null) {
// No declaration could be found for this identifier and its name matches a known TS helper // No declaration could be found for this identifier and its name matches a known TS helper
@ -186,17 +90,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
} }
} }
if (superDeclaration === null || superDeclaration.node === null ||
superDeclaration.known !== null) {
return superDeclaration;
}
// Get the identifier for the outer class node (if any).
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(superDeclaration.node);
const declaration = outerClassNode !== null ?
super.getDeclarationOfIdentifier(outerClassNode.name) :
superDeclaration;
if (declaration === null || declaration.node === null || declaration.known !== null) { if (declaration === null || declaration.node === null || declaration.known !== null) {
return declaration; return declaration;
} }
@ -235,23 +128,24 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @returns an object containing the node, statements and parameters of the function. * @returns an object containing the node, statements and parameters of the function.
*/ */
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) && const definition = super.getDefinitionOfFunction(node);
!ts.isFunctionExpression(node)) { if (definition === null) {
return null; return null;
} }
const parameters = // Filter out and capture parameter initializers
node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null})); if (definition.body !== null) {
let lookingForParamInitializers = true; let lookingForInitializers = true;
const statements = definition.body.filter(s => {
lookingForInitializers =
lookingForInitializers && captureParamInitializer(s, definition.parameters);
// If we are no longer looking for parameter initializers then we include this statement
return !lookingForInitializers;
});
definition.body = statements;
}
const statements = node.body && node.body.statements.filter(s => { return definition;
lookingForParamInitializers =
lookingForParamInitializers && reflectParamInitializer(s, parameters);
// If we are no longer looking for parameter initializers then we include this statement
return !lookingForParamInitializers;
});
return {node, body: statements || null, parameters};
} }
/** /**
@ -276,38 +170,32 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
/** /**
* Get the inner function declaration of an ES5-style class. * In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
* whose value is assigned to a variable (which represents the class to the rest of the program).
* So we might need to dig around to get hold of the "class" declaration.
* *
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE * This method extracts a `NgccClassSymbol` if `declaration` is the function declaration inside
* and returned to be assigned to a variable outside the IIFE, which is what the rest of the * the IIFE. Otherwise, undefined is returned.
* program interacts with.
* *
* Given the outer variable declaration, we want to get to the inner function declaration. * @param declaration the declaration whose symbol we are finding.
* * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
* @param decl a declaration node that could be the variable expression outside an ES5 class IIFE.
* @param checker the TS program TypeChecker
* @returns the inner function declaration or `undefined` if it is not a "class".
*/ */
protected getInnerFunctionDeclarationFromClassDeclaration(decl: ts.Declaration): protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
ts.FunctionDeclaration|undefined { const classSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
// Extract the IIFE body (if any). if (classSymbol !== undefined) {
const iifeBody = getIifeBody(decl); return classSymbol;
if (!iifeBody) return undefined; }
// Extract the function declaration from inside the IIFE. if (!ts.isFunctionDeclaration(declaration) || !hasNameIdentifier(declaration)) {
const functionDeclaration = iifeBody.statements.find(ts.isFunctionDeclaration); return undefined;
if (!functionDeclaration) return undefined; }
// Extract the return identifier of the IIFE. const outerDeclaration = getClassDeclarationFromInnerDeclaration(declaration);
const returnIdentifier = getReturnIdentifier(iifeBody); if (outerDeclaration === null || !hasNameIdentifier(outerDeclaration)) {
const returnIdentifierSymbol = return undefined;
returnIdentifier && this.checker.getSymbolAtLocation(returnIdentifier); }
if (!returnIdentifierSymbol) return undefined;
// Verify that the inner function is returned. return this.createClassSymbol(outerDeclaration, declaration);
if (returnIdentifierSymbol.valueDeclaration !== functionDeclaration) return undefined;
return functionDeclaration;
} }
/** /**
@ -524,83 +412,6 @@ 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):
ClassDeclaration<ts.VariableDeclaration>|null {
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 null;
// 2. IIFE function expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isFunctionExpression(outerNode)) return null;
// 3. IIFE call expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isCallExpression(outerNode)) return null;
// 4. Parenthesis around IIFE.
outerNode = outerNode.parent;
if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return null;
// 5. Outer variable declaration.
outerNode = outerNode.parent;
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return null;
// Finally, ensure that the variable declaration has a `name` identifier.
return hasNameIdentifier(outerNode) ? outerNode : null;
}
return null;
}
export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) {
return undefined;
}
// Recognize a variable declaration of one of the forms:
// - `var MyClass = (function () { ... }());`
// - `var MyClass = MyClass_1 = (function () { ... }());`
let parenthesizedCall = declaration.initializer;
while (isAssignment(parenthesizedCall)) {
parenthesizedCall = parenthesizedCall.right;
}
const body = getIifeConciseBody(parenthesizedCall);
return body !== undefined && ts.isBlock(body) ? body : undefined;
}
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {
const returnStatement = body.statements.find(ts.isReturnStatement);
if (!returnStatement || !returnStatement.expression) {
return undefined;
}
if (ts.isIdentifier(returnStatement.expression)) {
return returnStatement.expression;
}
if (isAssignment(returnStatement.expression) &&
ts.isIdentifier(returnStatement.expression.left)) {
return returnStatement.expression.left;
}
return undefined;
}
function getReturnStatement(declaration: ts.Expression|undefined): ts.ReturnStatement|undefined { function getReturnStatement(declaration: ts.Expression|undefined): ts.ReturnStatement|undefined {
return declaration && ts.isFunctionExpression(declaration) ? return declaration && ts.isFunctionExpression(declaration) ?
declaration.body.statements.find(ts.isReturnStatement) : declaration.body.statements.find(ts.isReturnStatement) :
@ -766,7 +577,7 @@ function isSuperIdentifier(node: ts.Node): boolean {
* @param parameters the collection of parameters that were found in the function definition * @param parameters the collection of parameters that were found in the function definition
* @returns true if the statement was a parameter initializer * @returns true if the statement was a parameter initializer
*/ */
function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) { function captureParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) && if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) { ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
const ifStatementComparison = statement.expression; // (arg === void 0) const ifStatementComparison = statement.expression; // (arg === void 0)
@ -792,3 +603,34 @@ function isUndefinedComparison(expression: ts.Expression): expression is ts.Expr
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken && expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left); ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
} }
/**
* Parse the declaration of the given `classSymbol` to find the IIFE wrapper function.
*
* This function may accept a `_super` argument if there is a base class.
*
* ```
* var TestClass = (function (_super) {
* __extends(TestClass, _super);
* function TestClass() {}
* return TestClass;
* }(BaseClass));
* ```
*
* @param classSymbol the class whose iife wrapper function we want to get.
* @returns the IIFE function or null if it could not be parsed.
*/
function getIifeFn(classSymbol: NgccClassSymbol|undefined): ts.FunctionExpression|null {
if (classSymbol === undefined) {
return null;
}
const innerDeclaration = classSymbol.implementation.valueDeclaration;
const iifeBody = innerDeclaration.parent;
if (!ts.isBlock(iifeBody)) {
return null;
}
const iifeWrapper = iifeBody.parent;
return iifeWrapper && ts.isFunctionExpression(iifeWrapper) ? iifeWrapper : null;
}

View File

@ -48,6 +48,12 @@ export interface NgccClassSymbol {
* declaration. * declaration.
*/ */
implementation: ts.Symbol; implementation: ts.Symbol;
/**
* Represents the symbol corresponding to a variable within a class IIFE that may be used to
* attach static properties or decorated.
*/
adjacent?: ts.Symbol;
} }
/** /**

View File

@ -14,7 +14,7 @@ import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
import {DelegatingReflectionHost} from '../../src/host/delegating_host'; import {DelegatingReflectionHost} from '../../src/host/delegating_host';
import {getIifeBody} from '../../src/host/esm5_host'; import {getIifeBody} from '../../src/host/esm2015_host';
import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host';
import {BundleProgram} from '../../src/packages/bundle_program'; import {BundleProgram} from '../../src/packages/bundle_program';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
@ -2212,7 +2212,8 @@ exports.MissingClass2 = MissingClass2;
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2227,7 +2228,8 @@ exports.MissingClass2 = MissingClass2;
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(innerNode); const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2243,7 +2245,8 @@ exports.MissingClass2 = MissingClass2;
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const innerSymbol = host.getClassSymbol(innerNode)!; const innerSymbol = host.getClassSymbol(innerNode)!;
const outerSymbol = host.getClassSymbol(outerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!;
@ -2260,7 +2263,8 @@ exports.MissingClass2 = MissingClass2;
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2277,7 +2281,8 @@ exports.MissingClass2 = MissingClass2;
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2345,7 +2350,8 @@ exports.MissingClass2 = MissingClass2;
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
expect(host.isClass(innerNode)).toBe(true); expect(host.isClass(innerNode)).toBe(true);
}); });

View File

@ -133,6 +133,14 @@ runInEachFileSystem(() => {
class NoDecoratorConstructorClass { class NoDecoratorConstructorClass {
constructor(foo) {} constructor(foo) {}
} }
let SimpleWrappedClass = /** @class */ (() => {
class SimpleWrappedClassInner {}
return SimpleWrappedClassInner;
})();
let AliasedWrappedClass = /** @class */ (() => {
let AliasedWrappedClassAdjacent = class AliasedWrappedClassInner {};
return SimpleWrappedClassAdjacent;
})();
`, `,
}; };
@ -166,6 +174,11 @@ runInEachFileSystem(() => {
]; ];
return AliasedWrappedClass; return AliasedWrappedClass;
})(); })();
let DecoratedWrappedClass = /** @class */ (() => {
let DecoratedWrappedClass = class DecoratedWrappedClass {}
// ... add decorations ...
return DecoratedWrappedClass;
})();
let usageOfWrappedClass = AliasedWrappedClass_1; let usageOfWrappedClass = AliasedWrappedClass_1;
`, `,
}; };
@ -534,7 +547,27 @@ runInEachFileSystem(() => {
{ type: Directive, args: [{ selector: '[b]' }] } { type: Directive, args: [{ selector: '[b]' }] }
]; ];
class C {} class C {}
export { A, x, C }; var AliasedClass_1;
let AliasedClass = AliasedClass_1 = class AliasedClass {}
AliasedClass.decorators = [
{ type: Directive, args: [{ selector: '[aliased]' },] }
];
let Wrapped1 = /** @class */ (() => {
let Wrapped1 = class Wrapped1 {
};
Wrapped1 = __decorate([
Directive({selector: '[wrapped-1]'})
], Wrapped1);
return Wrapped1;
})();
let Wrapped2 = /** @class */ (() => {
class Wrapped2 {}
Wrapped2.decorators = [
{ type: Directive, args: [{ selector: '[wrapped-2]' },] }
];
return Wrapped2;
})();
export { A, x, C, AliasedClass, Wrapped1, Wrapped2 };
` `
}, },
{ {
@ -1605,6 +1638,30 @@ runInEachFileSystem(() => {
.toBe(classDeclaration); .toBe(classDeclaration);
}); });
it('should return the correct declaration for an inner class identifier inside an IIFE',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerDeclaration = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerDeclaration =
((((outerDeclaration.initializer as ts.CallExpression).expression as
ts.ParenthesizedExpression)
.expression as ts.ArrowFunction)
.body as ts.Block)
.statements[0] as ts.ClassDeclaration;
const outerIdentifier = outerDeclaration.name as ts.Identifier;
const innerIdentifier = innerDeclaration.name as ts.Identifier;
expect(host.getDeclarationOfIdentifier(outerIdentifier)!.node).toBe(outerDeclaration);
expect(host.getDeclarationOfIdentifier(innerIdentifier)!.node).toBe(outerDeclaration);
});
it('should recognize enum declarations with string values', () => { it('should recognize enum declarations with string values', () => {
const testFile: TestFile = { const testFile: TestFile = {
name: _('/node_modules/test-package/some/file.js'), name: _('/node_modules/test-package/some/file.js'),
@ -1767,6 +1824,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(node); expect(classSymbol!.declaration.valueDeclaration).toBe(node);
expect(classSymbol!.implementation.valueDeclaration).toBe(node); expect(classSymbol!.implementation.valueDeclaration).toBe(node);
expect(classSymbol!.adjacent).toBeUndefined();
}); });
it('should return the class symbol for a class expression (outer variable declaration)', it('should return the class symbol for a class expression (outer variable declaration)',
@ -1784,6 +1842,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode); expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode); expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode);
expect(classSymbol!.adjacent).toBeUndefined();
}); });
it('should return the class symbol for a class expression (inner class expression)', () => { it('should return the class symbol for a class expression (inner class expression)', () => {
@ -1798,6 +1857,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode); expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode); expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode);
expect(classSymbol!.adjacent).toBeUndefined();
}); });
it('should return the same class symbol (of the outer declaration) for outer and inner declarations', it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
@ -1837,6 +1897,7 @@ runInEachFileSystem(() => {
return fail('Expected a named class declaration'); return fail('Expected a named class declaration');
} }
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
expect(classSymbol.adjacent).toBeUndefined();
}); });
it('should return the class symbol for a wrapped class expression (inner class expression)', it('should return the class symbol for a wrapped class expression (inner class expression)',
@ -1861,6 +1922,7 @@ runInEachFileSystem(() => {
return fail('Expected a named class declaration'); return fail('Expected a named class declaration');
} }
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass'); expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
expect(classSymbol.adjacent).toBeUndefined();
}); });
it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations', it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations',
@ -1881,6 +1943,87 @@ runInEachFileSystem(() => {
expect(innerSymbol.implementation).toBe(outerSymbol.implementation); expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
}); });
it('should return the class symbol for a decorated 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, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(outerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('DecoratedWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!ts.isClassExpression(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name!.text)
.toBe('DecoratedWrappedClass');
if (classSymbol.adjacent === undefined ||
!isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) {
return fail('Expected a named variable declaration for the adjacent symbol');
}
expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass');
});
it('should return the class symbol for a decorated 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, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const innerNode: ts.ClassExpression =
(outerNode as any)
.initializer.expression.expression.body.statements[0]
.declarationList.declarations[0]
.initializer;
const classSymbol = host.getClassSymbol(innerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('DecoratedWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol.implementation.valueDeclaration).toBe(innerNode);
if (classSymbol.adjacent === undefined ||
!isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) {
return fail('Expected a named variable declaration for the adjacent symbol');
}
expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass');
});
it('should return the same class symbol (of the outer declaration) for decorated 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, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const innerNode: ts.ClassExpression =
(outerNode as any)
.initializer.expression.expression.body.statements[0]
.declarationList.declarations[0]
.initializer;
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', () => { it('should return undefined if node is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]); loadTestFiles([FOO_FUNCTION_FILE]);
const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name); const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
@ -1892,7 +2035,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeUndefined(); expect(classSymbol).toBeUndefined();
}); });
it('should return undefined if variable declaration is not initialized using a class expression', it('should return undefined if variable declaration is not initialized to a valid class definition',
() => { () => {
const testFile = { const testFile = {
name: _('/test.js'), name: _('/test.js'),
@ -2115,8 +2258,9 @@ runInEachFileSystem(() => {
const secondaryFile = getSourceFileOrError(bundle.program, DECORATED_FILES[1].name); const secondaryFile = getSourceFileOrError(bundle.program, DECORATED_FILES[1].name);
const classSymbolsPrimary = host.findClassSymbols(primaryFile); const classSymbolsPrimary = host.findClassSymbols(primaryFile);
expect(classSymbolsPrimary.length).toEqual(3); expect(classSymbolsPrimary.map(c => c.name)).toEqual([
expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B', 'C']); 'A', 'B', 'C', 'AliasedClass', 'Wrapped1', 'Wrapped2'
]);
const classSymbolsSecondary = host.findClassSymbols(secondaryFile); const classSymbolsSecondary = host.findClassSymbols(secondaryFile);
expect(classSymbolsSecondary.length).toEqual(1); expect(classSymbolsSecondary.length).toEqual(1);
@ -2134,10 +2278,14 @@ runInEachFileSystem(() => {
const classSymbolsPrimary = host.findClassSymbols(primaryFile); const classSymbolsPrimary = host.findClassSymbols(primaryFile);
const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s)); const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s));
expect(classDecoratorsPrimary.length).toEqual(3);
expect(classDecoratorsPrimary.length).toEqual(6);
expect(classDecoratorsPrimary[0]!.map(d => d.name)).toEqual(['Directive']); expect(classDecoratorsPrimary[0]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[1]!.map(d => d.name)).toEqual(['Directive']); expect(classDecoratorsPrimary[1]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[2]).toBe(null); expect(classDecoratorsPrimary[2]).toBe(null);
expect(classDecoratorsPrimary[3]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[4]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[5]!.map(d => d.name)).toEqual(['Directive']);
const classSymbolsSecondary = host.findClassSymbols(secondaryFile); const classSymbolsSecondary = host.findClassSymbols(secondaryFile);
const classDecoratorsSecondary = const classDecoratorsSecondary =
@ -2339,7 +2487,7 @@ runInEachFileSystem(() => {
}); });
describe('getInternalNameOfClass()', () => { describe('getInternalNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => { it('should return the name of the class (if there is no separate inner class)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]); loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
@ -2347,10 +2495,32 @@ runInEachFileSystem(() => {
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass'); expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass');
}); });
it('should return the name of the inner class (if there is an IIFE)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('SimpleWrappedClassInner');
});
it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('AliasedWrappedClassInner');
});
}); });
describe('getAdjacentNameOfClass()', () => { describe('getAdjacentNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => { it('should return the name of the class (if there is no separate inner class)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]); loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name); const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
@ -2358,6 +2528,28 @@ runInEachFileSystem(() => {
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass'); expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass');
}); });
it('should return the name of the inner class (if there is an IIFE)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('SimpleWrappedClassInner');
});
it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('AliasedWrappedClassAdjacent');
});
}); });
describe('getEndOfClass()', () => { describe('getEndOfClass()', () => {

View File

@ -12,7 +12,8 @@ import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/test
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing'; import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {getIifeBody} from '../../src/host/esm2015_host';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils'; import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils';
@ -331,7 +332,8 @@ export { AliasedDirective$1 };
const classNode = getDeclaration( const classNode = getDeclaration(
bundle.program, _('/some_minified_directive.js'), 'SomeDirective', bundle.program, _('/some_minified_directive.js'), 'SomeDirective',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const innerNode = getIifeBody(classNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(classNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(classNode); const classSymbol = host.getClassSymbol(classNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();

View File

@ -14,8 +14,8 @@ import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, Downleve
import {getDeclaration} from '../../../src/ngtsc/testing'; import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {DelegatingReflectionHost} from '../../src/host/delegating_host'; import {DelegatingReflectionHost} from '../../src/host/delegating_host';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost, getIifeBody} from '../../src/host/esm2015_host';
import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host';
import {BundleProgram} from '../../src/packages/bundle_program'; import {BundleProgram} from '../../src/packages/bundle_program';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
@ -2283,7 +2283,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2297,7 +2298,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(innerNode); const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2312,7 +2314,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const innerSymbol = host.getClassSymbol(innerNode)!; const innerSymbol = host.getClassSymbol(innerNode)!;
const outerSymbol = host.getClassSymbol(outerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!;
@ -2327,7 +2330,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2343,7 +2347,8 @@ runInEachFileSystem(() => {
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2403,7 +2408,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
expect(host.isClass(innerNode)).toBe(true); expect(host.isClass(innerNode)).toBe(true);
}); });

View File

@ -14,7 +14,7 @@ import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Im
import {getDeclaration} from '../../../src/ngtsc/testing'; import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {DelegatingReflectionHost} from '../../src/host/delegating_host'; import {DelegatingReflectionHost} from '../../src/host/delegating_host';
import {getIifeBody} from '../../src/host/esm5_host'; import {getIifeBody} from '../../src/host/esm2015_host';
import {NgccReflectionHost} from '../../src/host/ngcc_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host';
import {parseStatementForUmdModule, UmdReflectionHost} from '../../src/host/umd_host'; import {parseStatementForUmdModule, UmdReflectionHost} from '../../src/host/umd_host';
import {BundleProgram} from '../../src/packages/bundle_program'; import {BundleProgram} from '../../src/packages/bundle_program';
@ -2399,7 +2399,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2413,7 +2414,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(innerNode); const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2428,7 +2430,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const innerSymbol = host.getClassSymbol(innerNode)!; const innerSymbol = host.getClassSymbol(innerNode)!;
const outerSymbol = host.getClassSymbol(outerNode)!; const outerSymbol = host.getClassSymbol(outerNode)!;
@ -2443,7 +2446,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'NoParensClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2459,7 +2463,8 @@ runInEachFileSystem(() => {
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass', bundle.program, SIMPLE_CLASS_FILE.name, 'InnerParensClass',
isNamedVariableDeclaration); isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
const classSymbol = host.getClassSymbol(outerNode); const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined(); expect(classSymbol).toBeDefined();
@ -2519,7 +2524,8 @@ runInEachFileSystem(() => {
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration( const outerNode = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode = getIifeBody(outerNode)!.statements.find(isNamedFunctionDeclaration)!; const innerNode = (getIifeBody(outerNode.initializer!) as ts.Block)
.statements.find(isNamedFunctionDeclaration)!;
expect(host.isClass(innerNode)).toBe(true); expect(host.isClass(innerNode)).toBe(true);
}); });