fix(ngcc): support TS 3.9 wrapped ES2015 classes (#36884)

In TS 3.9 the compiler will start to wrap ES2015 classes in an IIFE to help with
tree-shaking when the class has "associated" statements.

E.g.

```ts
let PlatformLocation = /** @class */ (() => {
    ...
    class PlatformLocation {
    }
    ...
    return PlatformLocation;
})();
```

This commit updates `Esm2015ReflectionHost` to support this format.

PR Close #36884
This commit is contained in:
Pete Bacon Darwin
2020-05-01 14:33:31 +01:00
committed by Alex Rickabaugh
parent 58ea040570
commit db4c59dad9
3 changed files with 256 additions and 54 deletions

View File

@ -30,6 +30,7 @@ runInEachFileSystem(() => {
let ACCESSORS_FILE: TestFile;
let SIMPLE_CLASS_FILE: TestFile;
let CLASS_EXPRESSION_FILE: TestFile;
let WRAPPED_CLASS_EXPRESSION_FILE: TestFile;
let FOO_FUNCTION_FILE: TestFile;
let INVALID_DECORATORS_FILE: TestFile;
let INVALID_DECORATOR_ARGS_FILE: TestFile;
@ -150,6 +151,26 @@ runInEachFileSystem(() => {
`,
};
WRAPPED_CLASS_EXPRESSION_FILE = {
name: _('/wrapped_class_expression.js'),
contents: `
import {Directive} from '@angular/core';
var AliasedWrappedClass_1;
let SimpleWrappedClass = /** @class */ (() => {
class SimpleWrappedClass {}
return SimpleWrappedClass;
})();
let AliasedWrappedClass = AliasedWrappedClass_1 = /** @class */ (() => {
class AliasedWrappedClass {}
AliasedWrappedClass.decorators = [
{ type: Directive, args: [{ selector: '[someDirective]' },] }
];
return AliasedWrappedClass;
})();
let usageOfWrappedClass = AliasedWrappedClass_1;
`,
};
FOO_FUNCTION_FILE = {
name: _('/foo_function.js'),
contents: `
@ -762,6 +783,26 @@ runInEachFileSystem(() => {
]);
});
it('should find the decorators on an aliased wrapped class', () => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const classNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode)!;
expect(decorators).not.toBe(null!);
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args!.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should return null if the symbol is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
@ -1620,6 +1661,22 @@ runInEachFileSystem(() => {
.toBe(classDeclaration);
});
it('should return the original declaration of an aliased class', () => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const classDeclaration = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'AliasedWrappedClass',
ts.isVariableDeclaration);
const usageOfWrappedClass = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'usageOfWrappedClass',
ts.isVariableDeclaration);
const aliasedClassIdentifier = usageOfWrappedClass.initializer as ts.Identifier;
expect(aliasedClassIdentifier.text).toBe('AliasedWrappedClass_1');
expect(host.getDeclarationOfIdentifier(aliasedClassIdentifier)!.node)
.toBe(classDeclaration);
});
it('should recognize enum declarations with string values', () => {
const testFile: TestFile = {
name: _('/node_modules/test-package/some/file.js'),
@ -1832,6 +1889,70 @@ runInEachFileSystem(() => {
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return the class symbol for a wrapped class expression (outer variable declaration)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(outerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('SimpleWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
});
it('should return the class symbol for a wrapped class expression (inner class expression)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block)
.statements[0];
const classSymbol = host.getClassSymbol(innerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('SimpleWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!isNamedClassDeclaration(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
});
it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerNode = ((outerNode as any).initializer.expression.expression.body as ts.Block)
.statements[0];
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 bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);