fix(ivy): handle class declarations consistently in ES5 code (#29209)

PR Close #29209
This commit is contained in:
George Kalpakas 2019-03-20 12:10:58 +02:00 committed by Miško Hevery
parent 2790352d04
commit 21835af70c
7 changed files with 332 additions and 205 deletions

View File

@ -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(

View File

@ -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.

View File

@ -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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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');