fix(ivy): ngcc - properly handle aliases class expressions (#29119)

In ES2015, classes could have been emitted as a variable declaration
initialized with a class expression. In certain situations, an intermediary
variable suffixed with `_1` is present such that the variable
declaration's initializer becomes a binary expression with its rhs being
the class expression, and its lhs being the identifier of the intermediate
variable. This structure was not recognized, resulting in such classes not
being considered as a class in `Esm2015ReflectionHost`.

As a consequence, the analysis of functions/methods that return a
`ModuleWithProviders` object did not take the methods of such classes into
account.

Another edge-case with such intermediate variable was that static
properties would not be considered as class members. A testcase was added
to prevent regressions.

Fixes #29078

PR Close #29119
This commit is contained in:
JoostK 2019-03-05 23:29:28 +01:00 committed by Jason Aden
parent 5a1d21ebb4
commit 98f8b0f328
12 changed files with 431 additions and 74 deletions

View File

@ -9,9 +9,9 @@ import * as ts from 'typescript';
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
import {Reference} from '../../../src/ngtsc/imports';
import {Declaration} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, Declaration} from '../../../src/ngtsc/reflection';
import {ModuleWithProvidersFunction, NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
import {hasNameIdentifier, isDefined} from '../utils';
export interface ModuleWithProvidersInfo {
/**
@ -23,7 +23,7 @@ export interface ModuleWithProvidersInfo {
/**
* The NgModule class declaration (in the .d.ts file) to add as a type parameter.
*/
ngModule: Declaration;
ngModule: Declaration<ClassDeclaration>;
}
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
@ -44,11 +44,7 @@ export class ModuleWithProvidersAnalyzer {
null;
if (!typeParam || isAnyKeyword(typeParam)) {
// Either we do not have a parameterized type or the type is `any`.
let ngModule = this.host.getDeclarationOfIdentifier(fn.ngModule);
if (!ngModule) {
throw new Error(
`Cannot find a declaration for NgModule ${fn.ngModule.text} referenced in ${fn.declaration.getText()}`);
}
let ngModule = fn.ngModule;
// For internal (non-library) module references, redirect the module's value declaration
// to its type declaration.
if (ngModule.viaModule === null) {
@ -57,14 +53,12 @@ export class ModuleWithProvidersAnalyzer {
throw new Error(
`No typings declaration can be found for the referenced NgModule class in ${fn.declaration.getText()}.`);
}
if (!ts.isClassDeclaration(dtsNgModule)) {
if (!ts.isClassDeclaration(dtsNgModule) || !hasNameIdentifier(dtsNgModule)) {
throw new Error(
`The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
`The referenced NgModule in ${fn.declaration.getText()} is not a named class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
}
// Record the usage of the internal module as it needs to become an exported symbol
const reference = new Reference(ngModule.node);
reference.addIdentifier(fn.ngModule);
this.referencesRegistry.add(ngModule.node, reference);
this.referencesRegistry.add(ngModule.node, new Reference(ngModule.node));
ngModule = {node: dtsNgModule, viaModule: null};
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
@ -50,6 +50,29 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
*/
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
protected dtsDeclarationMap: Map<string, ts.Declaration>|null;
/**
* The set of source files that have already been preprocessed.
*/
protected preprocessedSourceFiles = new Set<ts.SourceFile>();
/**
* In ES2015, class declarations may have been down-leveled into variable declarations,
* initialized using a class expression. In certain scenarios, an additional variable
* is introduced that represents the class so that results in code such as:
*
* ```
* let MyClass_1; let MyClass = MyClass_1 = class MyClass {};
* ```
*
* This map tracks those aliased variables to their original identifier, i.e. the key
* corresponds with the declaration of `MyClass_1` and its value becomes the `MyClass` identifier
* of the variable declaration.
*
* This map is populated during the preprocessing of each source file.
*/
protected aliasedClassDeclarations = new Map<ts.Declaration, ts.Identifier>();
constructor(
protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker,
dts?: BundleProgram|null) {
@ -62,19 +85,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* Classes should have a `name` identifier, because they may need to be referenced in other parts
* of the program.
*
* In ES2015, a class may be declared using a variable declaration of the following structure:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* 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 {
if (ts.isVariableDeclaration(node) && node.initializer) {
node = node.initializer;
}
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
return undefined;
}
return hasNameIdentifier(node) ? node : undefined;
return getInnerClassDeclaration(node) || undefined;
}
/**
@ -155,6 +179,60 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return null;
}
hasBaseClass(clazz: ClassDeclaration): boolean {
const superHasBaseClass = super.hasBaseClass(clazz);
if (superHasBaseClass) {
return superHasBaseClass;
}
const innerClassDeclaration = getInnerClassDeclaration(clazz);
if (innerClassDeclaration === null) {
return false;
}
return innerClassDeclaration.heritageClauses !== undefined &&
innerClassDeclaration.heritageClauses.some(
clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
}
/**
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ClassDeclaration {
return super.isClass(node) || !!this.getClassDeclaration(node);
}
/**
* Trace an identifier to its declaration, if possible.
*
* This method attempts to resolve the declaration of the given identifier, tracing back through
* imports and re-exports until the original declaration statement is found. A `Declaration`
* object is returned if the original declaration is found, or `null` is returned otherwise.
*
* In ES2015, we need to account for identifiers that refer to aliased class declarations such as
* `MyClass_1`. Since such declarations are only available within the module itself, we need to
* find the original class declaration, e.g. `MyClass`, that is associated with the aliased one.
*
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
*
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
* otherwise.
*/
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
const superDeclaration = super.getDeclarationOfIdentifier(id);
// The identifier may have been of an additional class assignment such as `MyClass_1` that was
// present as alias for `MyClass`. If so, resolve such aliases to their original declaration.
if (superDeclaration !== null) {
const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node);
if (aliasedIdentifier !== null) {
return this.getDeclarationOfIdentifier(aliasedIdentifier);
}
}
return superDeclaration;
}
/**
* Search the given module for variable declarations in which the initializer
* is an identifier marked with the `PRE_R3_MARKER`.
@ -341,6 +419,74 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
///////////// Protected Helpers /////////////
/**
* Finds the identifier of the actual class declaration for a potentially aliased declaration of a
* class.
*
* If the given declaration is for an alias of a class, this function will determine an identifier
* to the original declaration that represents this class.
*
* @param declaration The declaration to resolve.
* @returns The original identifier that the given class declaration resolves to, or `undefined`
* if the declaration does not represent an aliased class.
*/
protected resolveAliasedClassIdentifier(declaration: ts.Declaration): ts.Identifier|null {
this.ensurePreprocessed(declaration.getSourceFile());
return this.aliasedClassDeclarations.get(declaration) || null;
}
/**
* Ensures that the source file that `node` is part of has been preprocessed.
*
* During preprocessing, all statements in the source file will be visited such that certain
* processing steps can be done up-front and cached for subsequent usages.
*
* @param sourceFile The source file that needs to have gone through preprocessing.
*/
protected ensurePreprocessed(sourceFile: ts.SourceFile): void {
if (!this.preprocessedSourceFiles.has(sourceFile)) {
this.preprocessedSourceFiles.add(sourceFile);
for (const statement of sourceFile.statements) {
this.preprocessStatement(statement);
}
}
}
/**
* Analyzes the given statement to see if it corresponds with a variable declaration like
* `let MyClass = MyClass_1 = class MyClass {};`. If so, the declaration of `MyClass_1`
* is associated with the `MyClass` identifier.
*
* @param statement The statement that needs to be preprocessed.
*/
protected preprocessStatement(statement: ts.Statement): void {
if (!ts.isVariableStatement(statement)) {
return;
}
const declarations = statement.declarationList.declarations;
if (declarations.length !== 1) {
return;
}
const declaration = declarations[0];
const initializer = declaration.initializer;
if (!ts.isIdentifier(declaration.name) || !initializer || !isAssignment(initializer) ||
!ts.isIdentifier(initializer.left) || !ts.isClassExpression(initializer.right)) {
return;
}
const aliasedIdentifier = initializer.left;
const aliasedDeclaration = this.getDeclarationOfIdentifier(aliasedIdentifier);
if (aliasedDeclaration === null) {
throw new Error(
`Unable to locate declaration of ${aliasedIdentifier.text} in "${statement.getText()}"`);
}
this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name);
}
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
if (decoratorsProperty) {
@ -492,8 +638,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
// }
// MyClass.staticProperty = ...;
// ```
if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) {
const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name);
const variableDeclaration = getVariableDeclarationOfDeclaration(symbol.valueDeclaration);
if (variableDeclaration !== undefined) {
const variableSymbol = this.checker.getSymbolAtLocation(variableDeclaration.name);
if (variableSymbol && variableSymbol.exports) {
variableSymbol.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
@ -703,7 +850,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
/**
* Reflect over the given array node and extract decorator information from each element.
*
@ -1166,12 +1312,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
protected parseForModuleWithProviders(
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
container: ts.Declaration|null = null): ModuleWithProvidersFunction|null {
const declaration = implementation &&
(ts.isFunctionDeclaration(implementation) || ts.isMethodDeclaration(implementation) ||
ts.isFunctionExpression(implementation)) ?
implementation :
null;
const body = declaration ? this.getDefinitionOfFunction(declaration).body : null;
if (implementation === null ||
(!ts.isFunctionDeclaration(implementation) && !ts.isMethodDeclaration(implementation) &&
!ts.isFunctionExpression(implementation))) {
return null;
}
const declaration = implementation;
const body = this.getDefinitionOfFunction(declaration).body;
const lastStatement = body && body[body.length - 1];
const returnExpression =
lastStatement && ts.isReturnStatement(lastStatement) && lastStatement.expression || null;
@ -1180,10 +1327,27 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
prop =>
!!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') ||
null;
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
const ngModuleIdentifier = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
null;
return ngModule && declaration && {name, ngModule, declaration, container};
// If no `ngModule` property was found in an object literal return value, return `null` to
// indicate that the provided node does not appear to be a `ModuleWithProviders` function.
if (ngModuleIdentifier === null) {
return null;
}
const ngModuleDeclaration = this.getDeclarationOfIdentifier(ngModuleIdentifier);
if (!ngModuleDeclaration) {
throw new Error(
`Cannot find a declaration for NgModule ${ngModuleIdentifier.text} referenced in "${declaration!.getText()}"`);
}
if (!hasNameIdentifier(ngModuleDeclaration.node)) {
return null;
}
const ngModule = ngModuleDeclaration as Declaration<ClassDeclaration>;
return {name, ngModule, declaration, container};
}
}
@ -1209,10 +1373,8 @@ export function isAssignmentStatement(statement: ts.Statement): statement is Ass
ts.isIdentifier(statement.expression.left);
}
export function isAssignment(expression: ts.Expression):
expression is ts.AssignmentExpression<ts.EqualsToken> {
return ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken;
export function isAssignment(node: ts.Node): node is ts.AssignmentExpression<ts.EqualsToken> {
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken;
}
/**
@ -1264,6 +1426,38 @@ function getCalleeName(call: ts.CallExpression): string|null {
///////////// Internal Helpers /////////////
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* 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.
*
* @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)) {
return null;
}
return hasNameIdentifier(node) ? node : null;
}
function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] {
// The arguments of a decorator are held in the `args` property of its declaration object.
const argsProperty = node.properties.filter(ts.isPropertyAssignment)
@ -1333,6 +1527,31 @@ function collectExportedDeclarations(
}
}
/**
* Attempt to resolve the variable declaration that the given declaration is assigned to.
* For example, for the following code:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* and the provided declaration being `class MyClass {}`, this will return the `var MyClass`
* declaration.
*
* @param declaration The declaration for which any variable declaration should be obtained.
* @returns the outer variable declaration if found, undefined otherwise.
*/
function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.VariableDeclaration|
undefined {
let node = declaration.parent;
// Detect an intermediary variable assignment and skip over it.
if (isAssignment(node) && ts.isIdentifier(node.left)) {
node = node.parent;
}
return ts.isVariableDeclaration(node) ? node : undefined;
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {isFromDtsFile} from '../../../src/ngtsc/util/src/typescript';
import {getNameText, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
@ -33,11 +34,6 @@ import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignme
*
*/
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
/**
* Check whether the given node actually represents a class.
*/
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".
*
@ -180,7 +176,10 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
if (super.isClass(clazz)) return super.getMembersOfClass(clazz);
// 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);

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ClassSymbol, ReflectionHost} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassSymbol, Declaration, ReflectionHost} from '../../../src/ngtsc/reflection';
import {DecoratedClass} from './decorated_class';
export const PRE_R3_MARKER = '__PRE_R3__';
@ -37,9 +37,10 @@ export interface ModuleWithProvidersFunction {
*/
container: ts.Declaration|null;
/**
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
* The declaration of the class that the `ngModule` property on the `ModuleWithProviders` object
* refers to.
*/
ngModule: ts.Identifier;
ngModule: Declaration<ClassDeclaration>;
}
/**

View File

@ -204,7 +204,7 @@ export abstract class Renderer {
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
importManager: ImportManager): void {
moduleWithProviders.forEach(info => {
const ngModuleName = (info.ngModule.node as ts.ClassDeclaration).name !.text;
const ngModuleName = info.ngModule.node.name.text;
const declarationFile = info.declaration.getSourceFile().fileName;
const ngModuleFile = info.ngModule.node.getSourceFile().fileName;
const importPath = info.ngModule.viaModule ||

View File

@ -44,7 +44,7 @@ export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.No
/**
* Does the given declaration have a name which is an identifier?
* @param declaration The declaration to test.
* @returns true if the declaration has an identifer for a name.
* @returns true if the declaration has an identifier for a name.
*/
export function hasNameIdentifier(declaration: ts.Declaration): declaration is ts.Declaration&
{name: ts.Identifier} {

View File

@ -85,6 +85,7 @@ const FILES = [
};
}
};
HttpClientXsrfModule.staticProperty = 'static';
HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
NgModule({
providers: [],
@ -224,6 +225,22 @@ describe('Fesm2015ReflectionHost [import helper style]', () => {
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should find static properties on a class that has an intermediate variable assignment',
() => {
const program = makeTestProgram(fileSystem.files[2]);
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/ngmodule.js', 'HttpClientXsrfModule', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});

View File

@ -83,6 +83,16 @@ const SIMPLE_CLASS_FILE = {
`,
};
const CLASS_EXPRESSION_FILE = {
name: '/class_expression.js',
contents: `
var AliasedClass_1;
let EmptyClass = class EmptyClass {};
let AliasedClass = AliasedClass_1 = class AliasedClass {}
let usageOfAliasedClass = AliasedClass_1;
`,
};
const FOO_FUNCTION_FILE = {
name: '/foo_function.js',
contents: `
@ -551,7 +561,17 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [
}
`
},
{name: '/src/module', contents: 'export class ExternalModule {}'},
{
name: '/src/aliased_class.js',
contents: `
var AliasedModule_1;
let AliasedModule = AliasedModule_1 = class AliasedModule {
static forRoot() { return { ngModule: AliasedModule_1 }; }
};
export { AliasedModule };
`
},
{name: '/src/module.js', contents: 'export class ExternalModule {}'},
];
describe('Esm2015ReflectionHost', () => {
@ -1327,6 +1347,18 @@ describe('Esm2015ReflectionHost', () => {
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
it('should return the original declaration of an aliased class', () => {
const program = makeTestProgram(CLASS_EXPRESSION_FILE);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classDeclaration = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration);
const usageOfAliasedClass = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'usageOfAliasedClass', ts.isVariableDeclaration);
const aliasedClassIdentifier = usageOfAliasedClass.initializer as ts.Identifier;
expect(aliasedClassIdentifier.text).toBe('AliasedClass_1');
expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier) !.node).toBe(classDeclaration);
});
});
describe('getExportsOfModule()', () => {
@ -1373,6 +1405,23 @@ describe('Esm2015ReflectionHost', () => {
expect(host.isClass(node)).toBe(true);
});
it('should return true if a given node is a class expression assigned into a variable', () => {
const program = makeTestProgram(CLASS_EXPRESSION_FILE);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
expect(host.isClass(node)).toBe(true);
});
it('should return true if a given node is a class expression assigned into two variables',
() => {
const program = makeTestProgram(CLASS_EXPRESSION_FILE);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const node = getDeclaration(
program, CLASS_EXPRESSION_FILE.name, 'AliasedClass', ts.isVariableDeclaration);
expect(host.isClass(node)).toBe(true);
});
it('should return false if a given node is a TS function declaration', () => {
const program = makeTestProgram(FOO_FUNCTION_FILE);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -1382,6 +1431,46 @@ describe('Esm2015ReflectionHost', () => {
});
});
describe('hasBaseClass()', () => {
it('should not consider a class without extends clause as having a base class', () => {
const file = {
name: '/base_class.js',
contents: `class TestClass {}`,
};
const program = makeTestProgram(file);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration);
expect(host.hasBaseClass(classNode)).toBe(false);
});
it('should consider a class with extends clause as having a base class', () => {
const file = {
name: '/base_class.js',
contents: `
class BaseClass {}
class TestClass extends BaseClass {}`,
};
const program = makeTestProgram(file);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(program, file.name, 'TestClass', isNamedClassDeclaration);
expect(host.hasBaseClass(classNode)).toBe(true);
});
it('should consider an aliased class with extends clause as having a base class', () => {
const file = {
name: '/base_class.js',
contents: `
let TestClass_1;
class BaseClass {}
let TestClass = TestClass_1 = class TestClass extends BaseClass {}`,
};
const program = makeTestProgram(file);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration);
expect(host.hasBaseClass(classNode)).toBe(true);
});
});
describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => {
const program = makeTestProgram(ARITY_CLASSES[0]);
@ -1554,12 +1643,13 @@ describe('Esm2015ReflectionHost', () => {
new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/functions.js') !;
const fns = host.getModuleWithProvidersFunctions(file);
expect(fns.map(info => [info.declaration.name !.getText(), info.ngModule.text])).toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
expect(fns.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text]))
.toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
});
it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object',
@ -1569,12 +1659,24 @@ describe('Esm2015ReflectionHost', () => {
new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/methods.js') !;
const fn = host.getModuleWithProvidersFunctions(file);
expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.text])).toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text]))
.toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
});
// https://github.com/angular/angular/issues/29078
it('should resolve aliased module references to their original declaration', () => {
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
const host = new Esm2015ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/aliased_class.js') !;
const fn = host.getModuleWithProvidersFunctions(file);
expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])).toEqual([
['forRoot', 'AliasedModule'],
]);
});
});
});

View File

@ -450,4 +450,4 @@ function findIdentifier(
function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean {
return ts.isPropertyAssignment(identifier.parent) &&
identifier.parent.name.getText() === 'ngModule';
}
}

View File

@ -700,7 +700,20 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [
export {SomeService, InternalModule};
`
},
{name: '/src/module', contents: 'export class ExternalModule {}'},
{
name: '/src/aliased_class.js',
contents: `
var AliasedModule = (function() {
function AliasedModule() {}
AliasedModule_1 = AliasedModule;
AliasedModule.forRoot = function() { return { ngModule: AliasedModule_1 }; };
var AliasedModule_1;
return AliasedModule;
}());
export { AliasedModule };
`
},
{name: '/src/module.js', contents: 'export class ExternalModule {}'},
];
describe('Esm5ReflectionHost', () => {
@ -1832,12 +1845,13 @@ describe('Esm5ReflectionHost', () => {
const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/functions.js') !;
const fns = host.getModuleWithProvidersFunctions(file);
expect(fns.map(info => [info.declaration.name !.getText(), info.ngModule.text])).toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
expect(fns.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text]))
.toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
});
it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object',
@ -1846,7 +1860,7 @@ describe('Esm5ReflectionHost', () => {
const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/methods.js') !;
const fn = host.getModuleWithProvidersFunctions(file);
expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.text])).toEqual([
expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([
[
'function() { return { ngModule: InternalModule }; }',
'InternalModule',
@ -1865,5 +1879,16 @@ describe('Esm5ReflectionHost', () => {
],
]);
});
// https://github.com/angular/angular/issues/29078
it('should resolve aliased module references to their original declaration', () => {
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
const host = new Esm5ReflectionHost(new MockLogger(), false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/aliased_class.js') !;
const fn = host.getModuleWithProvidersFunctions(file);
expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([
['function() { return { ngModule: AliasedModule_1 }; }', 'AliasedModule'],
]);
});
});
});

View File

@ -317,11 +317,11 @@ export interface Import {
* The declaration of a symbol, along with information about how it was imported into the
* application.
*/
export interface Declaration {
export interface Declaration<T extends ts.Declaration = ts.Declaration> {
/**
* TypeScript reference to the declaration itself.
*/
node: ts.Declaration;
node: T;
/**
* The absolute module path from which the symbol was imported into the application, if the symbol

View File

@ -25,7 +25,7 @@ export function isFromDtsFile(node: ts.Node): boolean {
if (sf === undefined) {
sf = ts.getOriginalNode(node).getSourceFile();
}
return sf !== undefined && D_TS.test(sf.fileName);
return sf !== undefined && sf.isDeclarationFile;
}
export function nodeNameForError(node: ts.Node & {name?: ts.Node}): string {