fix(ivy): handle class declarations consistently in ES5 code (#29209)
PR Close #29209
This commit is contained in:
parent
2790352d04
commit
21835af70c
|
@ -17,7 +17,10 @@ export class DecoratedClass {
|
|||
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
|
||||
* @param name The name of the class that has been found. This is mostly used
|
||||
* for informational purposes.
|
||||
* @param declaration The TypeScript AST node where this class is declared
|
||||
* @param declaration The TypeScript AST node where this class is declared. In ES5 code, where a
|
||||
* class can be represented by both a variable declaration and a function declaration (inside an
|
||||
* IIFE), `declaration` will always refer to the outer variable declaration, which represents the
|
||||
* class to the rest of the program.
|
||||
* @param decorators The collection of decorators that have been found on this class.
|
||||
*/
|
||||
constructor(
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
|||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {BundleProgram} from '../packages/bundle_program';
|
||||
import {findAll, getNameText, isDefined} from '../utils';
|
||||
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
|
||||
|
||||
import {DecoratedClass} from './decorated_class';
|
||||
import {ModuleWithProvidersFunction, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
|
||||
|
@ -54,6 +54,37 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
this.dtsDeclarationMap = dts && this.computeDtsDeclarationMap(dts.path, dts.program) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the declaration of a node that we think is a class.
|
||||
* Classes should have a `name` identifier, because they may need to be referenced in other parts
|
||||
* of the program.
|
||||
*
|
||||
* @param node the node that represents the class whose declaration we are finding.
|
||||
* @returns the declaration of the class or `undefined` if it is not a "class".
|
||||
*/
|
||||
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
node = node.initializer;
|
||||
}
|
||||
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return hasNameIdentifier(node) ? node : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a symbol for a node that we think is a class.
|
||||
* @param node the node whose symbol we are finding.
|
||||
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
|
||||
*/
|
||||
getClassSymbol(declaration: ts.Node): ClassSymbol|undefined {
|
||||
const classDeclaration = this.getClassDeclaration(declaration);
|
||||
return classDeclaration &&
|
||||
this.checker.getSymbolAtLocation(classDeclaration.name) as ClassSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a declaration (for example, of a class or function) and return metadata about any
|
||||
* decorators present on the declaration.
|
||||
|
@ -86,79 +117,12 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
* @throws if `declaration` does not resolve to a class declaration.
|
||||
*/
|
||||
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
|
||||
const members: ClassMember[] = [];
|
||||
const symbol = this.getClassSymbol(clazz);
|
||||
if (!symbol) {
|
||||
const classSymbol = this.getClassSymbol(clazz);
|
||||
if (!classSymbol) {
|
||||
throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`);
|
||||
}
|
||||
|
||||
// The decorators map contains all the properties that are decorated
|
||||
const decoratorsMap = this.getMemberDecorators(symbol);
|
||||
|
||||
// The member map contains all the method (instance and static); and any instance properties
|
||||
// that are initialized in the class.
|
||||
if (symbol.members) {
|
||||
symbol.members.forEach((value, key) => {
|
||||
const decorators = decoratorsMap.get(key as string);
|
||||
const reflectedMembers = this.reflectMembers(value, decorators);
|
||||
if (reflectedMembers) {
|
||||
decoratorsMap.delete(key as string);
|
||||
members.push(...reflectedMembers);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The static property map contains all the static properties
|
||||
if (symbol.exports) {
|
||||
symbol.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If this class was declared as a VariableDeclaration then it may have static properties
|
||||
// attached to the variable rather than the class itself
|
||||
// For example:
|
||||
// ```
|
||||
// let MyClass = class MyClass {
|
||||
// // no static properties here!
|
||||
// }
|
||||
// MyClass.staticProperty = ...;
|
||||
// ```
|
||||
if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) {
|
||||
const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name);
|
||||
if (variableSymbol && variableSymbol.exports) {
|
||||
variableSymbol.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
|
||||
decoratorsMap.forEach((value, key) => {
|
||||
members.push({
|
||||
implementation: null,
|
||||
decorators: value,
|
||||
isStatic: false,
|
||||
kind: ClassMemberKind.Property,
|
||||
name: key,
|
||||
nameNode: null,
|
||||
node: null,
|
||||
type: null,
|
||||
value: null
|
||||
});
|
||||
});
|
||||
|
||||
return members;
|
||||
return this.getMembersOfSymbol(classSymbol);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -188,24 +152,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a symbol for a node that we think is a class.
|
||||
* @param node the node whose symbol we are finding.
|
||||
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
|
||||
*/
|
||||
getClassSymbol(declaration: ts.Node): ClassSymbol|undefined {
|
||||
if (ts.isClassDeclaration(declaration)) {
|
||||
return declaration.name && this.checker.getSymbolAtLocation(declaration.name) as ClassSymbol;
|
||||
}
|
||||
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
||||
declaration = declaration.initializer;
|
||||
}
|
||||
if (ts.isClassExpression(declaration)) {
|
||||
return declaration.name && this.checker.getSymbolAtLocation(declaration.name) as ClassSymbol;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the given module for variable declarations in which the initializer
|
||||
* is an identifier marked with the `PRE_R3_MARKER`.
|
||||
|
@ -497,6 +443,84 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
return decorators.length ? decorators : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a symbol which should be of a class, and return metadata about its members.
|
||||
*
|
||||
* @param symbol the `ClassSymbol` representing the class over which to reflect.
|
||||
* @returns an array of `ClassMember` metadata representing the members of the class.
|
||||
*/
|
||||
protected getMembersOfSymbol(symbol: ClassSymbol): ClassMember[] {
|
||||
const members: ClassMember[] = [];
|
||||
|
||||
// The decorators map contains all the properties that are decorated
|
||||
const decoratorsMap = this.getMemberDecorators(symbol);
|
||||
|
||||
// The member map contains all the method (instance and static); and any instance properties
|
||||
// that are initialized in the class.
|
||||
if (symbol.members) {
|
||||
symbol.members.forEach((value, key) => {
|
||||
const decorators = decoratorsMap.get(key as string);
|
||||
const reflectedMembers = this.reflectMembers(value, decorators);
|
||||
if (reflectedMembers) {
|
||||
decoratorsMap.delete(key as string);
|
||||
members.push(...reflectedMembers);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The static property map contains all the static properties
|
||||
if (symbol.exports) {
|
||||
symbol.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If this class was declared as a VariableDeclaration then it may have static properties
|
||||
// attached to the variable rather than the class itself
|
||||
// For example:
|
||||
// ```
|
||||
// let MyClass = class MyClass {
|
||||
// // no static properties here!
|
||||
// }
|
||||
// MyClass.staticProperty = ...;
|
||||
// ```
|
||||
if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) {
|
||||
const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name);
|
||||
if (variableSymbol && variableSymbol.exports) {
|
||||
variableSymbol.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
|
||||
decoratorsMap.forEach((value, key) => {
|
||||
members.push({
|
||||
implementation: null,
|
||||
decorators: value,
|
||||
isStatic: false,
|
||||
kind: ClassMemberKind.Property,
|
||||
name: key,
|
||||
nameNode: null,
|
||||
node: null,
|
||||
type: null,
|
||||
value: null
|
||||
});
|
||||
});
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the member decorators for the given class.
|
||||
* @param classSymbol the class whose member decorators we are interested in.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {getNameText, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
|
||||
|
@ -36,9 +36,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|||
/**
|
||||
* Check whether the given node actually represents a class.
|
||||
*/
|
||||
isClass(node: ts.Node): node is ClassDeclaration {
|
||||
return super.isClass(node) || !!this.getClassSymbol(node);
|
||||
}
|
||||
isClass(node: ts.Node): node is ClassDeclaration { return !!this.getClassDeclaration(node); }
|
||||
|
||||
/**
|
||||
* Determines whether the given declaration, which should be a "class", has a base "class".
|
||||
|
@ -48,11 +46,13 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|||
* @param clazz a `ClassDeclaration` representing the class over which to reflect.
|
||||
*/
|
||||
hasBaseClass(clazz: ClassDeclaration): boolean {
|
||||
const classSymbol = this.getClassSymbol(clazz);
|
||||
if (!classSymbol) return false;
|
||||
if (super.hasBaseClass(clazz)) return true;
|
||||
|
||||
const iifeBody = classSymbol.valueDeclaration.parent;
|
||||
if (!iifeBody || !ts.isBlock(iifeBody)) return false;
|
||||
const classDeclaration = this.getClassDeclaration(clazz);
|
||||
if (!classDeclaration) return false;
|
||||
|
||||
const iifeBody = getIifeBody(classDeclaration);
|
||||
if (!iifeBody) return false;
|
||||
|
||||
const iife = iifeBody.parent;
|
||||
if (!iife || !ts.isFunctionExpression(iife)) return false;
|
||||
|
@ -61,38 +61,39 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find a symbol for a node that we think is a class.
|
||||
* Find the declaration of a class given a node that we think represents the class.
|
||||
*
|
||||
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
|
||||
* So we might need to dig around inside to get hold of the "class" symbol.
|
||||
* 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.
|
||||
*
|
||||
* `node` might be one of:
|
||||
* - A class declaration (from a declaration file).
|
||||
* - A class declaration (from a typings file).
|
||||
* - The declaration of the outer variable, which is assigned the result of the IIFE.
|
||||
* - The function declaration inside the IIFE, which is eventually returned and assigned to the
|
||||
* outer variable.
|
||||
*
|
||||
* @param node the top level declaration that represents an exported class or the function
|
||||
* expression inside the IIFE.
|
||||
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
|
||||
* The returned declaration is either the class declaration (from the typings file) or the outer
|
||||
* variable declaration.
|
||||
*
|
||||
* @param node the node that represents the class whose declaration we are finding.
|
||||
* @returns the declaration of the class or `undefined` if it is not a "class".
|
||||
*/
|
||||
getClassSymbol(node: ts.Node): ClassSymbol|undefined {
|
||||
const symbol = super.getClassSymbol(node);
|
||||
if (symbol) return symbol;
|
||||
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
|
||||
const superDeclaration = super.getClassDeclaration(node);
|
||||
if (superDeclaration) return superDeclaration;
|
||||
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
const iifeBody = getIifeBody(node);
|
||||
if (!iifeBody) return undefined;
|
||||
const outerClass = getClassDeclarationFromInnerFunctionDeclaration(node);
|
||||
if (outerClass) return outerClass;
|
||||
|
||||
const innerClassIdentifier = getReturnIdentifier(iifeBody);
|
||||
if (!innerClassIdentifier) return undefined;
|
||||
|
||||
return this.checker.getSymbolAtLocation(innerClassIdentifier) as ClassSymbol;
|
||||
// At this point, `node` could be the outer variable declaration of an ES5 class.
|
||||
// If so, ensure that it has a `name` identifier and the correct structure.
|
||||
if (!isNamedVariableDeclaration(node) ||
|
||||
!this.getInnerFunctionDeclarationFromClassDeclaration(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node);
|
||||
|
||||
return outerClassNode && this.getClassSymbol(outerClassNode);
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,12 +116,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|||
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
||||
// Get the identifier for the outer class node (if any).
|
||||
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent);
|
||||
|
||||
if (outerClassNode && hasNameIdentifier(outerClassNode)) {
|
||||
id = outerClassNode.name;
|
||||
}
|
||||
|
||||
const declaration = super.getDeclarationOfIdentifier(id);
|
||||
const declaration = super.getDeclarationOfIdentifier(outerClassNode ? outerClassNode.name : id);
|
||||
|
||||
if (!declaration || !ts.isVariableDeclaration(declaration.node) ||
|
||||
declaration.node.initializer !== undefined ||
|
||||
|
@ -172,31 +168,149 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
|||
return {node, body: statements || null, parameters};
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a declaration which should be of a class, and return metadata about the members of the
|
||||
* class.
|
||||
*
|
||||
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
|
||||
* reflect.
|
||||
*
|
||||
* @returns an array of `ClassMember` metadata representing the members of the class.
|
||||
*
|
||||
* @throws if `declaration` does not resolve to a class declaration.
|
||||
*/
|
||||
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
|
||||
if (super.isClass(clazz)) return super.getMembersOfClass(clazz);
|
||||
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol = this.getInnerFunctionSymbolFromClassDeclaration(clazz);
|
||||
if (!innerFunctionSymbol) {
|
||||
throw new Error(
|
||||
`Attempted to get members of a non-class: "${(clazz as ClassDeclaration).getText()}"`);
|
||||
}
|
||||
|
||||
return this.getMembersOfSymbol(innerFunctionSymbol);
|
||||
}
|
||||
|
||||
|
||||
///////////// 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
|
||||
* and returned to be assigned to a variable outside the IIFE, which is what the rest of the
|
||||
* program interacts with.
|
||||
*
|
||||
* Given the outer variable declaration, we want to get to the inner function declaration.
|
||||
*
|
||||
* @param node a 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(node: ts.Node): ts.FunctionDeclaration
|
||||
|undefined {
|
||||
if (!ts.isVariableDeclaration(node)) return undefined;
|
||||
|
||||
// Extract the IIFE body (if any).
|
||||
const iifeBody = getIifeBody(node);
|
||||
if (!iifeBody) return undefined;
|
||||
|
||||
// Extract the function declaration from inside the IIFE.
|
||||
const functionDeclaration = iifeBody.statements.find(ts.isFunctionDeclaration);
|
||||
if (!functionDeclaration) return undefined;
|
||||
|
||||
// Extract the return identifier of the IIFE.
|
||||
const returnIdentifier = getReturnIdentifier(iifeBody);
|
||||
const returnIdentifierSymbol =
|
||||
returnIdentifier && this.checker.getSymbolAtLocation(returnIdentifier);
|
||||
if (!returnIdentifierSymbol) return undefined;
|
||||
|
||||
// Verify that the inner function is returned.
|
||||
if (returnIdentifierSymbol.valueDeclaration !== functionDeclaration) return undefined;
|
||||
|
||||
return functionDeclaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identifier symbol of 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
|
||||
* and returned to be assigned to a variable outside the IIFE, which is what the rest of the
|
||||
* program interacts with.
|
||||
*
|
||||
* Given the outer variable declaration, we want to get to the identifier symbol of the inner
|
||||
* function declaration.
|
||||
*
|
||||
* @param clazz a node that could be the variable expression outside an ES5 class IIFE.
|
||||
* @param checker the TS program TypeChecker
|
||||
* @returns the inner function declaration identifier symbol or `undefined` if it is not a "class"
|
||||
* or has no identifier.
|
||||
*/
|
||||
protected getInnerFunctionSymbolFromClassDeclaration(clazz: ClassDeclaration): ClassSymbol
|
||||
|undefined {
|
||||
const innerFunctionDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(clazz);
|
||||
if (!innerFunctionDeclaration || !hasNameIdentifier(innerFunctionDeclaration)) return undefined;
|
||||
|
||||
return this.checker.getSymbolAtLocation(innerFunctionDeclaration.name) as ClassSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the declarations of the constructor parameters of a class identified by its symbol.
|
||||
*
|
||||
* In ESM5 there is no "class" so the constructor that we want is actually the declaration
|
||||
* function itself.
|
||||
* In ESM5, there is no "class" so the constructor that we want is actually the inner function
|
||||
* declaration inside the IIFE, whose return value is assigned to the outer variable declaration
|
||||
* (that represents the class to the rest of the program).
|
||||
*
|
||||
* @param classSymbol the class whose parameters we want to find.
|
||||
* @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose
|
||||
* parameters we want to find.
|
||||
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
|
||||
* the class's constructor or null if there is no constructor.
|
||||
* the class's constructor or `null` if there is no constructor.
|
||||
*/
|
||||
protected getConstructorParameterDeclarations(classSymbol: ClassSymbol):
|
||||
ts.ParameterDeclaration[]|null {
|
||||
const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration;
|
||||
const constructor =
|
||||
this.getInnerFunctionDeclarationFromClassDeclaration(classSymbol.valueDeclaration);
|
||||
if (!constructor) return null;
|
||||
|
||||
if (constructor.parameters.length > 0) {
|
||||
return Array.from(constructor.parameters);
|
||||
}
|
||||
|
||||
if (isSynthesizedConstructor(constructor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter decorators of a class constructor.
|
||||
*
|
||||
* @param classSymbol the symbol of the class (i.e. the outer variable declaration) whose
|
||||
* parameter info we want to get.
|
||||
* @param parameterNodes the array of TypeScript parameter nodes for this class's constructor.
|
||||
* @returns an array of constructor parameter info objects.
|
||||
*/
|
||||
protected getConstructorParamInfo(
|
||||
classSymbol: ClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] {
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol =
|
||||
this.getInnerFunctionSymbolFromClassDeclaration(classSymbol.valueDeclaration);
|
||||
if (!innerFunctionSymbol) return [];
|
||||
|
||||
return super.getConstructorParamInfo(innerFunctionSymbol, parameterNodes);
|
||||
}
|
||||
|
||||
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
|
||||
// The necessary info is on the inner function declaration (inside the ES5 class IIFE).
|
||||
const innerFunctionSymbol =
|
||||
this.getInnerFunctionSymbolFromClassDeclaration(symbol.valueDeclaration);
|
||||
if (!innerFunctionSymbol) return null;
|
||||
|
||||
return super.getDecoratorsOfSymbol(innerFunctionSymbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter type and decorators for the constructor of a class,
|
||||
* where the information is stored on a static method of the class.
|
||||
|
@ -389,8 +503,8 @@ function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name
|
|||
* @param node a node that could be the function expression inside an ES5 class IIFE.
|
||||
* @returns the outer variable declaration or `undefined` if it is not a "class".
|
||||
*/
|
||||
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.VariableDeclaration|
|
||||
undefined {
|
||||
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node):
|
||||
ClassDeclaration<ts.VariableDeclaration>|undefined {
|
||||
if (ts.isFunctionDeclaration(node)) {
|
||||
// It might be the function expression inside the IIFE. We need to go 5 levels up...
|
||||
|
||||
|
@ -414,14 +528,16 @@ function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.Vari
|
|||
outerNode = outerNode.parent;
|
||||
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined;
|
||||
|
||||
return outerNode;
|
||||
// Finally, ensure that the variable declaration has a `name` identifier.
|
||||
return hasNameIdentifier(outerNode) ? outerNode : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined {
|
||||
if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) {
|
||||
export function getIifeBody(declaration: ts.Declaration): ts.Block|undefined {
|
||||
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer ||
|
||||
!ts.isParenthesizedExpression(declaration.initializer)) {
|
||||
return undefined;
|
||||
}
|
||||
const call = declaration.initializer;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
import * as ts from 'typescript';
|
||||
import MagicString from 'magic-string';
|
||||
import {getIifeBody} from '../host/esm5_host';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {CompiledClass} from '../analysis/decoration_analyzer';
|
||||
import {EsmRenderer} from './esm_renderer';
|
||||
|
@ -23,21 +24,18 @@ export class Esm5Renderer extends EsmRenderer {
|
|||
* Add the definitions to each decorated class
|
||||
*/
|
||||
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
|
||||
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
|
||||
if (!classSymbol) {
|
||||
throw new Error(
|
||||
`Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent;
|
||||
if (!parent || !ts.isBlock(parent)) {
|
||||
const iifeBody = getIifeBody(compiledClass.declaration);
|
||||
if (!iifeBody) {
|
||||
throw new Error(
|
||||
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement));
|
||||
|
||||
const returnStatement = iifeBody.statements.find(ts.isReturnStatement);
|
||||
if (!returnStatement) {
|
||||
throw new Error(
|
||||
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
|
||||
const insertionPoint = returnStatement.getFullStart();
|
||||
output.appendLeft(insertionPoint, '\n' + definitions);
|
||||
}
|
||||
|
|
|
@ -1536,7 +1536,7 @@ describe('Esm2015ReflectionHost', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getModuleWithProvidersFunctions', () => {
|
||||
describe('getModuleWithProvidersFunctions()', () => {
|
||||
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
|
||||
() => {
|
||||
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMemberKind, ClassSymbol, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
|
||||
import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host';
|
||||
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
|
||||
|
||||
import {expectTypeValueReferencesForParameters} from './util';
|
||||
|
@ -96,6 +96,13 @@ const ACCESSORS_FILE = {
|
|||
`,
|
||||
};
|
||||
|
||||
const SIMPLE_ES2015_CLASS_FILE = {
|
||||
name: '/simple_es2015_class.d.ts',
|
||||
contents: `
|
||||
export class EmptyClass {}
|
||||
`,
|
||||
};
|
||||
|
||||
const SIMPLE_CLASS_FILE = {
|
||||
name: '/simple_class.js',
|
||||
contents: `
|
||||
|
@ -1095,7 +1102,7 @@ describe('Esm5ReflectionHost', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getConstructorParameters', () => {
|
||||
describe('getConstructorParameters()', () => {
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
const program = makeTestProgram(SOME_DIRECTIVE_FILE);
|
||||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
|
@ -1408,7 +1415,7 @@ describe('Esm5ReflectionHost', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getImportOfIdentifier', () => {
|
||||
describe('getImportOfIdentifier()', () => {
|
||||
it('should find the import of an identifier', () => {
|
||||
const program = makeTestProgram(...IMPORTS_FILES);
|
||||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
|
@ -1440,7 +1447,7 @@ describe('Esm5ReflectionHost', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getDeclarationOfIdentifier', () => {
|
||||
describe('getDeclarationOfIdentifier()', () => {
|
||||
it('should return the declaration of a locally defined identifier', () => {
|
||||
const program = makeTestProgram(SOME_DIRECTIVE_FILE);
|
||||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
|
@ -1550,21 +1557,15 @@ describe('Esm5ReflectionHost', () => {
|
|||
});
|
||||
|
||||
describe('getClassSymbol()', () => {
|
||||
let superGetClassSymbolSpy: jasmine.Spy;
|
||||
it('should return the class symbol for an ES2015 class', () => {
|
||||
const program = makeTestProgram(SIMPLE_ES2015_CLASS_FILE);
|
||||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
const node = getDeclaration(
|
||||
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
beforeEach(() => {
|
||||
superGetClassSymbolSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassSymbol');
|
||||
});
|
||||
|
||||
it('should return the class symbol returned by the superclass (if any)', () => {
|
||||
const mockNode = {} as ts.Node;
|
||||
const mockSymbol = {} as ClassSymbol;
|
||||
superGetClassSymbolSpy.and.returnValue(mockSymbol);
|
||||
|
||||
const host = new Esm5ReflectionHost(false, {} as any);
|
||||
|
||||
expect(host.getClassSymbol(mockNode)).toBe(mockSymbol);
|
||||
expect(superGetClassSymbolSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
|
||||
|
@ -1572,7 +1573,10 @@ describe('Esm5ReflectionHost', () => {
|
|||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
expect(host.getClassSymbol(node)).toBeDefined();
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(node);
|
||||
});
|
||||
|
||||
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
|
||||
|
@ -1580,27 +1584,22 @@ describe('Esm5ReflectionHost', () => {
|
|||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
const outerNode =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode =
|
||||
(((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression)
|
||||
.expression as ts.FunctionExpression)
|
||||
.body.statements.find(ts.isFunctionDeclaration) !;
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
const classSymbol = host.getClassSymbol(innerNode);
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBeDefined();
|
||||
expect(classSymbol).toBeDefined();
|
||||
expect(classSymbol !.valueDeclaration).toBe(outerNode);
|
||||
});
|
||||
|
||||
it('should return the same class symbol (of the inner declaration) for outer and inner declarations',
|
||||
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
|
||||
() => {
|
||||
const program = makeTestProgram(SIMPLE_CLASS_FILE);
|
||||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
|
||||
const innerNode = (((outerNode.initializer as ts.ParenthesizedExpression)
|
||||
.expression as ts.CallExpression)
|
||||
.expression as ts.FunctionExpression)
|
||||
.body.statements.find(ts.isFunctionDeclaration) as ClassDeclaration;
|
||||
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
expect(host.getClassSymbol(innerNode) !.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return undefined if node is not an ES5 class', () => {
|
||||
|
@ -1608,48 +1607,47 @@ describe('Esm5ReflectionHost', () => {
|
|||
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
|
||||
const node =
|
||||
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
|
||||
expect(host.getClassSymbol(node)).toBeUndefined();
|
||||
const classSymbol = host.getClassSymbol(node);
|
||||
|
||||
expect(classSymbol).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClass()', () => {
|
||||
let host: Esm5ReflectionHost;
|
||||
let mockNode: ts.Node;
|
||||
let superIsClassSpy: jasmine.Spy;
|
||||
let getClassSymbolSpy: jasmine.Spy;
|
||||
let getClassDeclarationSpy: jasmine.Spy;
|
||||
let superGetClassDeclarationSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
host = new Esm5ReflectionHost(false, null as any);
|
||||
mockNode = {} as any;
|
||||
|
||||
superIsClassSpy = spyOn(Esm2015ReflectionHost.prototype, 'isClass');
|
||||
getClassSymbolSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassSymbol');
|
||||
getClassDeclarationSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassDeclaration');
|
||||
superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
|
||||
});
|
||||
|
||||
it('should return true if superclass returns true', () => {
|
||||
superIsClassSpy.and.returnValue(true);
|
||||
superGetClassDeclarationSpy.and.returnValue(true);
|
||||
getClassDeclarationSpy.and.callThrough();
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(superIsClassSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(getClassSymbolSpy).not.toHaveBeenCalled();
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
|
||||
it('should return true if it can find a symbol for the class', () => {
|
||||
superIsClassSpy.and.returnValue(false);
|
||||
getClassSymbolSpy.and.returnValue(true);
|
||||
it('should return true if it can find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(true);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(true);
|
||||
expect(superIsClassSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
|
||||
it('should return false if it cannot find a symbol for the class', () => {
|
||||
superIsClassSpy.and.returnValue(false);
|
||||
getClassSymbolSpy.and.returnValue(false);
|
||||
it('should return false if it cannot find a declaration for the class', () => {
|
||||
getClassDeclarationSpy.and.returnValue(false);
|
||||
|
||||
expect(host.isClass(mockNode)).toBe(false);
|
||||
expect(superIsClassSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode);
|
||||
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -258,28 +258,16 @@ SOME DEFINITION TEXT
|
|||
const {renderer, host, sourceFile, program} = setup(PROGRAM);
|
||||
const output = new MagicString(PROGRAM.contents);
|
||||
|
||||
const badSymbolDeclaration =
|
||||
getDeclaration(program, sourceFile.fileName, 'A', ts.isVariableDeclaration);
|
||||
const badSymbol: any = {name: 'BadSymbol', declaration: badSymbolDeclaration};
|
||||
const hostSpy = spyOn(host, 'getClassSymbol').and.returnValue(null);
|
||||
expect(() => renderer.addDefinitions(output, badSymbol, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError('Compiled class does not have a valid symbol: BadSymbol in /some/file.js');
|
||||
|
||||
|
||||
const noIifeDeclaration =
|
||||
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration);
|
||||
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
|
||||
hostSpy.and.returnValue({valueDeclaration: noIifeDeclaration});
|
||||
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError(
|
||||
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js');
|
||||
|
||||
const badIifeWrapper: any =
|
||||
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration);
|
||||
const badIifeDeclaration =
|
||||
badIifeWrapper.initializer.expression.expression.body.statements[0];
|
||||
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration);
|
||||
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
|
||||
hostSpy.and.returnValue({valueDeclaration: badIifeDeclaration});
|
||||
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError(
|
||||
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js');
|
||||
|
|
Loading…
Reference in New Issue