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

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