fix(ngcc): consistently use outer declaration for classes (#32539)

In ngcc's reflection hosts for compiled JS bundles, such as ESM2015,
special care needs to be taken for classes as there may be an outer
declaration (referred to as "declaration") and an inner declaration
(referred to as "implementation") for a given class. Therefore, there
will also be two `ts.Symbol`s bound per class, and ngcc needs to switch
between those declarations and symbols depending on where certain
information can be found.

Prior to this commit, the `NgccReflectionHost` interface had methods
`getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`.
These class symbols would be used to kick off compilation of components
using ngtsc, so it is important for these symbols to correspond with the
publicly visible outer declaration of the class. However, the ESM2015
reflection host used to return the `ts.Symbol` for the inner
declaration, if the class was declared as follows:

```javascript
var MyClass = class MyClass {};
```

For the above code, `Esm2015ReflectionHost.getClassSymbol` would return
the `ts.Symbol` corresponding with the `class MyClass {}` declaration,
whereas it should have corresponded with the `var MyClass` declaration.
As a consequence, no `NgModule` could be resolved for the component, so
no components/directives would be in scope for the component. This
resulted in errors during runtime.

This commit resolves the issue by introducing a `NgccClassSymbol` that
contains references to both the outer and inner `ts.Symbol`, instead of
just a single `ts.Symbol`. This avoids the unclarity of whether a
`ts.Symbol` corresponds with the outer or inner declaration.

More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw

Fixes #32078
Closes FW-1507

PR Close #32539
This commit is contained in:
JoostK 2019-09-03 21:26:58 +02:00 committed by Kara Erickson
parent 2279cb8dc0
commit 373e1337de
12 changed files with 517 additions and 299 deletions

View File

@ -80,7 +80,7 @@ export class ModuleWithProvidersAnalyzer {
let dtsFn: ts.Declaration|null = null;
const containerClass = fn.container && this.host.getClassSymbol(fn.container);
if (containerClass) {
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
const dtsClass = this.host.getDtsDeclaration(containerClass.declaration.valueDeclaration);
// Get the declaration of the matching static method
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
dtsClass.members

View File

@ -20,9 +20,9 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.Sour
}
export function analyzeDecorators(
symbol: NgccClassSymbol, decorators: Decorator[] | null,
classSymbol: NgccClassSymbol, decorators: Decorator[] | null,
handlers: DecoratorHandler<any, any>[]): AnalyzedClass|null {
const declaration = symbol.valueDeclaration;
const declaration = classSymbol.declaration.valueDeclaration;
const matchingHandlers = handlers
.map(handler => {
const detected = handler.detect(declaration, decorators);
@ -78,7 +78,7 @@ export function analyzeDecorators(
}
}
return {
name: symbol.name,
name: classSymbol.name,
declaration,
decorators,
matches,

View File

@ -64,7 +64,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
if (esm5HelperCalls.length > 0) {
return esm5HelperCalls;
} else {
const sourceFile = classSymbol.valueDeclaration.getSourceFile();
const sourceFile = classSymbol.declaration.valueDeclaration.getSourceFile();
return this.getTopLevelHelperCalls(sourceFile, helperName);
}
}

View File

@ -15,7 +15,7 @@ import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
import {ModuleWithProvidersFunction, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
import {ClassSymbol, ModuleWithProvidersFunction, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String;
export const PROP_DECORATORS = 'propDecorators' as ts.__String;
@ -91,7 +91,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
/**
* Find the declaration of a node that we think is a class.
* Find a symbol for 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.
*
@ -104,22 +104,111 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` node is returned as declaration of `MyClass`.
*
* @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 {
return getInnerClassDeclaration(node) || undefined;
}
/**
* Find a symbol for a node that we think is a class.
* @param node the node whose symbol we are finding.
* @param declaration the declaration 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): NgccClassSymbol|undefined {
const classDeclaration = this.getClassDeclaration(declaration);
return classDeclaration &&
this.checker.getSymbolAtLocation(classDeclaration.name) as NgccClassSymbol;
const symbol = this.getClassSymbolFromOuterDeclaration(declaration);
if (symbol !== undefined) {
return symbol;
}
return this.getClassSymbolFromInnerDeclaration(declaration);
}
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
*
* ```
* var MyClass = MyClass_1 = class 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 the declaration is a regular class declaration.
if (ts.isClassDeclaration(declaration) && hasNameIdentifier(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 structure:
*
* ```
* var MyClass = MyClass_1 = class 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 {
if (!ts.isClassExpression(declaration) || !hasNameIdentifier(declaration)) {
return undefined;
}
const outerDeclaration = getVariableDeclarationOfDeclaration(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: 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,
};
}
/**
@ -221,7 +310,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ClassDeclaration {
return super.isClass(node) || !!this.getClassDeclaration(node);
return super.isClass(node) || this.getClassSymbol(node) !== undefined;
}
/**
@ -549,7 +638,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*/
protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol
|undefined {
return symbol.exports && symbol.exports.get(propertyName);
return symbol.implementation.exports && symbol.implementation.exports.get(propertyName);
}
/**
@ -561,8 +650,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns all information of the decorators on the class.
*/
protected acquireDecoratorInfo(classSymbol: NgccClassSymbol): DecoratorInfo {
if (this.decoratorCache.has(classSymbol.valueDeclaration)) {
return this.decoratorCache.get(classSymbol.valueDeclaration) !;
const decl = classSymbol.declaration.valueDeclaration;
if (this.decoratorCache.has(decl)) {
return this.decoratorCache.get(decl) !;
}
// First attempt extracting decorators from static properties.
@ -572,7 +662,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
decoratorInfo = this.computeDecoratorInfoFromHelperCalls(classSymbol);
}
this.decoratorCache.set(classSymbol.valueDeclaration, decoratorInfo);
this.decoratorCache.set(decl, decoratorInfo);
return decoratorInfo;
}
@ -665,8 +755,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
// 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) => {
if (symbol.implementation.members) {
symbol.implementation.members.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators);
if (reflectedMembers) {
@ -677,8 +767,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
// The static property map contains all the static properties
if (symbol.exports) {
symbol.exports.forEach((value, key) => {
if (symbol.implementation.exports) {
symbol.implementation.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators, true);
if (reflectedMembers) {
@ -697,11 +787,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
// }
// MyClass.staticProperty = ...;
// ```
const variableDeclaration = getVariableDeclarationOfDeclaration(symbol.valueDeclaration);
if (variableDeclaration !== undefined) {
const variableSymbol = this.checker.getSymbolAtLocation(variableDeclaration.name);
if (variableSymbol && variableSymbol.exports) {
variableSymbol.exports.forEach((value, key) => {
if (ts.isVariableDeclaration(symbol.declaration.valueDeclaration)) {
if (symbol.declaration.exports) {
symbol.declaration.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators, true);
if (reflectedMembers) {
@ -1182,8 +1270,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*/
protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol):
ts.ParameterDeclaration[]|null {
if (classSymbol.members && classSymbol.members.has(CONSTRUCTOR)) {
const constructorSymbol = classSymbol.members.get(CONSTRUCTOR) !;
const members = classSymbol.implementation.members;
if (members && members.has(CONSTRUCTOR)) {
const constructorSymbol = members.get(CONSTRUCTOR) !;
// For some reason the constructor does not have a `valueDeclaration` ?!?
const constructor = constructorSymbol.declarations &&
constructorSymbol.declarations[0] as ts.ConstructorDeclaration | undefined;
@ -1314,7 +1403,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
return Array.from(classSymbol.valueDeclaration.getSourceFile().statements);
return Array.from(classSymbol.declaration.valueDeclaration.getSourceFile().statements);
}
/**
@ -1637,28 +1726,29 @@ function getCalleeName(call: ts.CallExpression): string|null {
* ```
*
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` expression is returned as declaration of `MyClass`. Note that if `node`
* represents a regular class declaration, it will be returned as-is.
* `class MyClass {}` expression is returned as declaration of `var MyClass`. If the variable
* is not initialized using a class expression, null is returned.
*
* @param node the node that represents the class whose declaration we are finding.
* @returns the declaration of the class or `null` if it is not a "class".
*/
function getInnerClassDeclaration(node: ts.Node):
ClassDeclaration<ts.ClassDeclaration|ts.ClassExpression>|null {
// Recognize a variable declaration of the form `var MyClass = class MyClass {}` or
// `var MyClass = MyClass_1 = class MyClass {};`
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
node = node.initializer;
while (isAssignment(node)) {
node = node.right;
}
}
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
function getInnerClassDeclaration(node: ts.Node): ClassDeclaration<ts.ClassExpression>|null {
if (!ts.isVariableDeclaration(node) || node.initializer === undefined) {
return null;
}
return hasNameIdentifier(node) ? node : null;
// Recognize a variable declaration of the form `var MyClass = class MyClass {}` or
// `var MyClass = MyClass_1 = class MyClass {};`
let expression = node.initializer;
while (isAssignment(expression)) {
expression = expression.right;
}
if (!ts.isClassExpression(expression) || !hasNameIdentifier(expression)) {
return null;
}
return expression;
}
function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] {

View File

@ -8,8 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {isFromDtsFile} from '../../../src/ngtsc/util/src/typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
@ -45,10 +44,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
hasBaseClass(clazz: ClassDeclaration): boolean {
if (super.hasBaseClass(clazz)) return true;
const classDeclaration = this.getClassDeclaration(clazz);
if (!classDeclaration) return false;
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
return false;
}
const iifeBody = getIifeBody(classDeclaration);
const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration);
if (!iifeBody) return false;
const iife = iifeBody.parent;
@ -63,10 +64,12 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return superBaseClassIdentifier;
}
const classDeclaration = this.getClassDeclaration(clazz);
if (!classDeclaration) return null;
const classSymbol = this.getClassSymbol(clazz);
if (classSymbol === undefined) {
return null;
}
const iifeBody = getIifeBody(classDeclaration);
const iifeBody = getIifeBody(classSymbol.declaration.valueDeclaration);
if (!iifeBody) return null;
const iife = iifeBody.parent;
@ -84,39 +87,61 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
}
/**
* 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,
* 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 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.
* This method extracts a `NgccClassSymbol` if `declaration` is the outer variable which is
* assigned the result of the IIFE. Otherwise, undefined is returned.
*
* 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".
* @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.
*/
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
const superDeclaration = super.getClassDeclaration(node);
if (superDeclaration) return superDeclaration;
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
const classSymbol = super.getClassSymbolFromOuterDeclaration(declaration);
if (classSymbol !== undefined) {
return classSymbol;
}
const outerClass = getClassDeclarationFromInnerFunctionDeclaration(node);
if (outerClass) return outerClass;
// 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)) {
if (!isNamedVariableDeclaration(declaration)) {
return undefined;
}
return node;
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 === undefined || !hasNameIdentifier(outerDeclaration)) {
return undefined;
}
return this.createClassSymbol(outerDeclaration, declaration);
}
/**
@ -211,43 +236,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return {node, body: statements || null, helper: 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[] {
// Do not follow ES5's resolution logic when the node resides in a .d.ts file.
if (isFromDtsFile(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);
}
/** Gets all decorators of the given class symbol. */
getDecoratorsOfSymbol(symbol: NgccClassSymbol): 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);
}
///////////// Protected Helpers /////////////
@ -288,29 +276,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
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): NgccClassSymbol
|undefined {
const innerFunctionDeclaration = this.getInnerFunctionDeclarationFromClassDeclaration(clazz);
if (!innerFunctionDeclaration || !hasNameIdentifier(innerFunctionDeclaration)) return undefined;
return this.checker.getSymbolAtLocation(innerFunctionDeclaration.name) as NgccClassSymbol;
}
/**
* Find the declarations of the constructor parameters of a class identified by its symbol.
*
@ -325,9 +290,8 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
*/
protected getConstructorParameterDeclarations(classSymbol: NgccClassSymbol):
ts.ParameterDeclaration[]|null {
const constructor =
this.getInnerFunctionDeclarationFromClassDeclaration(classSymbol.valueDeclaration);
if (!constructor) return null;
const constructor = classSymbol.implementation.valueDeclaration;
if (!ts.isFunctionDeclaration(constructor)) return null;
if (constructor.parameters.length > 0) {
return Array.from(constructor.parameters);
@ -340,24 +304,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
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: NgccClassSymbol, 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);
}
/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static method of the class.
@ -481,7 +427,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: NgccClassSymbol): ts.Statement[] {
const classDeclarationParent = classSymbol.valueDeclaration.parent;
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
@ -499,26 +445,14 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
*/
protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol
|undefined {
// The symbol corresponds with the inner function declaration. First lets see if the static
// property is set there.
const prop = super.getStaticProperty(symbol, propertyName);
// First lets see if the static property can be resolved from the inner class symbol.
const prop = symbol.implementation.exports && symbol.implementation.exports.get(propertyName);
if (prop !== undefined) {
return prop;
}
// Otherwise, obtain the outer variable declaration and resolve its symbol, in order to lookup
// static properties there.
const outerClass = getClassDeclarationFromInnerFunctionDeclaration(symbol.valueDeclaration);
if (outerClass === undefined) {
return undefined;
}
const outerSymbol = this.checker.getSymbolAtLocation(outerClass.name);
if (outerSymbol === undefined || outerSymbol.valueDeclaration === undefined) {
return undefined;
}
return super.getStaticProperty(outerSymbol as NgccClassSymbol, propertyName);
// Otherwise, lookup the static properties on the outer class symbol.
return symbol.declaration.exports && symbol.declaration.exports.get(propertyName);
}
}

View File

@ -47,7 +47,32 @@ export interface ModuleWithProvidersFunction {
* The symbol corresponding to a "class" declaration. I.e. a `ts.Symbol` whose `valueDeclaration` is
* a `ClassDeclaration`.
*/
export type NgccClassSymbol = ts.Symbol & {valueDeclaration: ClassDeclaration};
export type ClassSymbol = ts.Symbol & {valueDeclaration: ClassDeclaration};
/**
* A representation of a class that accounts for the potential existence of two `ClassSymbol`s for a
* given class, as the compiled JavaScript bundles that ngcc reflects on can have two declarations.
*/
export interface NgccClassSymbol {
/**
* The name of the class.
*/
name: string;
/**
* Represents the symbol corresponding with the outer declaration of the class. This should be
* considered the public class symbol, i.e. its declaration is visible to the rest of the program.
*/
declaration: ClassSymbol;
/**
* Represents the symbol corresponding with the inner declaration of the class, referred to as its
* "implementation". This is not necessarily a `ClassSymbol` but rather just a `ts.Symbol`, as the
* inner declaration does not need to satisfy the requirements imposed on a publicly visible class
* declaration.
*/
implementation: ts.Symbol;
}
/**
* A reflection host that has extra methods for looking at non-Typescript package formats
@ -59,7 +84,7 @@ export interface NgccReflectionHost extends ReflectionHost {
* @returns the symbol for the declaration or `undefined` if it is not
* a "class" or has no symbol.
*/
getClassSymbol(node: ts.Node): NgccClassSymbol|undefined;
getClassSymbol(declaration: ts.Node): NgccClassSymbol|undefined;
/**
* Search the given module for variable declarations in which the initializer

View File

@ -79,7 +79,7 @@ export class EsmRenderingFormatter implements RenderingFormatter {
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
const insertionPoint = classSymbol.declaration.valueDeclaration !.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}

View File

@ -15,8 +15,14 @@ import {NgccClassSymbol} from '../../src/host/ngcc_host';
describe('DefaultMigrationHost', () => {
describe('injectSyntheticDecorator()', () => {
const mockHost: any = {
getClassSymbol: (node: any): NgccClassSymbol | undefined =>
({ valueDeclaration: node, name: node.name.text } as any),
getClassSymbol: (node: any): NgccClassSymbol | undefined => {
const symbol = { valueDeclaration: node, name: node.name.text } as any;
return {
name: node.name.text,
declaration: symbol,
implementation: symbol,
};
},
};
const mockMetadata: any = {};
const mockEvaluator: any = {};

View File

@ -13,7 +13,6 @@ import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassD
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getIifeBody} from '../../src/host/esm5_host';
import {MockLogger} from '../helpers/mock_logger';
import {getRootFiles, makeTestBundleProgram, makeTestDtsBundleProgram} from '../helpers/utils';
@ -1712,19 +1711,22 @@ exports.ExternalModule = ExternalModule;
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
});
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
const outerNode = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
@ -1737,7 +1739,8 @@ exports.ExternalModule = ExternalModule;
const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(outerNode);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
@ -1751,7 +1754,10 @@ exports.ExternalModule = ExternalModule;
const innerNode =
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
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 an ES5 class', () => {
@ -1764,46 +1770,67 @@ exports.ExternalModule = ExternalModule;
expect(classSymbol).toBeUndefined();
});
it('should return undefined if variable declaration is not initialized using an IIFE',
() => {
const testFile = {
name: _('/test.js'),
contents: `var MyClass = null;`,
};
loadTestFiles([testFile]);
const {program, host: compilerHost} = makeTestBundleProgram(testFile.name);
const host =
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const node =
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeUndefined();
});
});
describe('isClass()', () => {
let host: CommonJsReflectionHost;
let mockNode: ts.Node;
let getClassDeclarationSpy: jasmine.Spy;
let superGetClassDeclarationSpy: jasmine.Spy;
beforeEach(() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
mockNode = {} as any;
getClassDeclarationSpy = spyOn(CommonJsReflectionHost.prototype, 'getClassDeclaration');
superGetClassDeclarationSpy =
spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
it('should return true if a given node is a TS class declaration', () => {
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
const {program, host: compilerHost} =
makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.isClass(node)).toBe(true);
});
it('should return true if superclass returns true', () => {
superGetClassDeclarationSpy.and.returnValue(true);
getClassDeclarationSpy.and.callThrough();
it('should return true if a given node is the outer variable declaration of a class',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
expect(host.isClass(node)).toBe(true);
});
expect(host.isClass(mockNode)).toBe(true);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
});
it('should return true if a given node is the inner variable declaration of a class',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const outerNode = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode =
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.isClass(innerNode)).toBe(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(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
});
it('should return false if it cannot find a declaration for the class', () => {
getClassDeclarationSpy.and.returnValue(false);
expect(host.isClass(mockNode)).toBe(false);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
it('should return false if a given node is a function declaration', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const node =
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
expect(host.isClass(node)).toBe(false);
});
});

View File

@ -1565,6 +1565,95 @@ runInEachFileSystem(() => {
});
});
describe('getClassSymbol()', () => {
it('should return the class symbol for an ES2015 class', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeDefined();
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
});
it('should return the class symbol for a class expression (outer variable declaration)',
() => {
loadTestFiles([CLASS_EXPRESSION_FILE]);
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const outerNode = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = (outerNode.initializer as ts.ClassExpression);
const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the class symbol for a class expression (inner class expression)', () => {
loadTestFiles([CLASS_EXPRESSION_FILE]);
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const outerNode = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = (outerNode.initializer as ts.ClassExpression);
const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
() => {
loadTestFiles([CLASS_EXPRESSION_FILE]);
const {program} = makeTestBundleProgram(CLASS_EXPRESSION_FILE.name);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const outerNode = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = (outerNode.initializer as ts.ClassExpression);
const innerSymbol = host.getClassSymbol(innerNode) !;
const outerSymbol = host.getClassSymbol(outerNode) !;
expect(innerSymbol.declaration).toBe(outerSymbol.declaration);
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return undefined if node is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeUndefined();
});
it('should return undefined if variable declaration is not initialized using a class expression',
() => {
const testFile = {
name: _('/test.js'),
contents: `var MyClass = null;`,
};
loadTestFiles([testFile]);
const {program} = makeTestBundleProgram(testFile.name);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeUndefined();
});
});
describe('isClass()', () => {
it('should return true if a given node is a TS class declaration', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);

View File

@ -1874,19 +1874,22 @@ runInEachFileSystem(() => {
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
});
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node = getDeclaration(
const outerNode = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
@ -1899,7 +1902,8 @@ runInEachFileSystem(() => {
const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(outerNode);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
@ -1911,7 +1915,10 @@ runInEachFileSystem(() => {
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
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 an ES5 class', () => {
@ -1924,43 +1931,58 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeUndefined();
});
it('should return undefined if variable declaration is not initialized using an IIFE', () => {
const testFile = {
name: _('/test.js'),
contents: `var MyClass = null;`,
};
loadTestFiles([testFile]);
const {program} = makeTestBundleProgram(testFile.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node = getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeUndefined();
});
});
describe('isClass()', () => {
let host: Esm5ReflectionHost;
let mockNode: ts.Node;
let getClassDeclarationSpy: jasmine.Spy;
let superGetClassDeclarationSpy: jasmine.Spy;
beforeEach(() => {
host = new Esm5ReflectionHost(new MockLogger(), false, null as any);
mockNode = {} as any;
getClassDeclarationSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassDeclaration');
superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
it('should return true if a given node is a TS class declaration', () => {
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node = getDeclaration(
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.isClass(node)).toBe(true);
});
it('should return true if superclass returns true', () => {
superGetClassDeclarationSpy.and.returnValue(true);
getClassDeclarationSpy.and.callThrough();
expect(host.isClass(mockNode)).toBe(true);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
it('should return true if a given node is the outer variable declaration of a class', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
expect(host.isClass(node)).toBe(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(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
it('should return true if a given node is the inner variable declaration of a class', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const outerNode =
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.isClass(innerNode)).toBe(true);
});
it('should return false if it cannot find a declaration for the class', () => {
getClassDeclarationSpy.and.returnValue(false);
expect(host.isClass(mockNode)).toBe(false);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
it('should return false if a given node is a function declaration', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const {program} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node =
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
expect(host.isClass(node)).toBe(false);
});
});

View File

@ -1783,19 +1783,22 @@ runInEachFileSystem(() => {
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(node);
expect(classSymbol !.implementation.valueDeclaration).toBe(node);
});
it('should return the class symbol for an ES5 class (outer variable declaration)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
const outerNode = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
const classSymbol = host.getClassSymbol(outerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(node);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the class symbol for an ES5 class (inner function declaration)', () => {
@ -1808,7 +1811,8 @@ runInEachFileSystem(() => {
const classSymbol = host.getClassSymbol(innerNode);
expect(classSymbol).toBeDefined();
expect(classSymbol !.valueDeclaration).toBe(outerNode);
expect(classSymbol !.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol !.implementation.valueDeclaration).toBe(innerNode);
});
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
@ -1821,7 +1825,10 @@ runInEachFileSystem(() => {
const innerNode =
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
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 an ES5 class', () => {
@ -1834,46 +1841,64 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeUndefined();
});
it('should return undefined if variable declaration is not initialized using an IIFE',
() => {
const testFile = {
name: _('/test.js'),
contents: `var MyClass = null;`,
};
loadTestFiles([testFile]);
const {program, host: compilerHost} = makeTestBundleProgram(testFile.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const node =
getDeclaration(program, testFile.name, 'MyClass', isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(node);
expect(classSymbol).toBeUndefined();
});
});
describe('isClass()', () => {
let host: UmdReflectionHost;
let mockNode: ts.Node;
let getClassDeclarationSpy: jasmine.Spy;
let superGetClassDeclarationSpy: jasmine.Spy;
beforeEach(() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
mockNode = {} as any;
getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration');
superGetClassDeclarationSpy =
spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration');
it('should return true if a given node is a TS class declaration', () => {
loadTestFiles([SIMPLE_ES2015_CLASS_FILE]);
const {program, host: compilerHost} =
makeTestBundleProgram(SIMPLE_ES2015_CLASS_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.isClass(node)).toBe(true);
});
it('should return true if superclass returns true', () => {
superGetClassDeclarationSpy.and.returnValue(true);
getClassDeclarationSpy.and.callThrough();
it('should return true if a given node is the outer variable declaration of a class',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const node = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
expect(host.isClass(node)).toBe(true);
});
expect(host.isClass(mockNode)).toBe(true);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
});
it('should return true if a given node is the inner variable declaration of a class',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const outerNode = getDeclaration(
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
const innerNode =
getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !;
expect(host.isClass(innerNode)).toBe(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(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
});
it('should return false if it cannot find a declaration for the class', () => {
getClassDeclarationSpy.and.returnValue(false);
expect(host.isClass(mockNode)).toBe(false);
expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode);
it('should return false if a given node is a function declaration', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const node =
getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration);
expect(host.isClass(node)).toBe(false);
});
});