Skip to content

Commit

Permalink
fix(ngcc): find decorated constructor params on IIFE wrapped classes (#…
Browse files Browse the repository at this point in the history
…37436)

Now in TS 3.9, classes in ES2015 can be wrapped in an IIFE.
This commit ensures that we still find the static properties that contain
decorator information, even if they are attached to the adjacent node
of the class, rather than the implementation or declaration.

Fixes #37330

PR Close #37436
  • Loading branch information
petebacondarwin authored and atscott committed Jun 5, 2020
1 parent 5af3144 commit 2cb3b66
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 6 deletions.
13 changes: 7 additions & 6 deletions packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Expand Up @@ -866,19 +866,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/**
* Try to retrieve the symbol of a static property on a class.
*
* In some cases, a static property can either be set on the inner declaration inside the class'
* IIFE, or it can be set on the outer variable declaration. Therefore, the host checks both
* places, first looking up the property on the inner symbol, and if the property is not found it
* will fall back to looking up the property on the outer symbol.
* In some cases, a static property can either be set on the inner (implementation or adjacent)
* declaration inside the class' IIFE, or it can be set on the outer variable declaration.
* Therefore, the host checks all places, first looking up the property on the inner symbols, and
* if the property is not found it will fall back to looking up the property on the outer symbol.
*
* @param symbol the class whose property we are interested in.
* @param propertyName the name of static property.
* @returns the symbol if it is found or `undefined` if not.
*/
protected getStaticProperty(symbol: NgccClassSymbol, propertyName: ts.__String): ts.Symbol
|undefined {
return symbol.implementation.exports && symbol.implementation.exports.get(propertyName) ||
symbol.declaration.exports && symbol.declaration.exports.get(propertyName);
return symbol.implementation.exports?.get(propertyName) ||
symbol.adjacent?.exports?.get(propertyName) ||
symbol.declaration.exports?.get(propertyName);
}

/**
Expand Down
Expand Up @@ -93,6 +93,36 @@ runInEachFileSystem(() => {
], SomeDirective);
export { SomeDirective };
`,
},
{
name: _('/some_directive_ctor_parameters_iife.js'),
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
const INJECTED_TOKEN = new InjectionToken('injected');
let ViewContainerRef = /** class */ (() => { class ViewContainerRef {} return ViewContainerRef; })();
let TemplateRef = /** class */ (() => { class TemplateRef {} return TemplateRef; })();
let SomeDirective = /** @class */ (() => {
let SomeDirective = class SomeDirective {
constructor(_viewContainer, _template, injected) {
this.input1 = '';
}
};
SomeDirective.ctorParameters = () => [
{ type: ViewContainerRef, },
{ type: TemplateRef, },
{ type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
];
tslib_1.__decorate([
Input(),
], SomeDirective.prototype, "input1", void 0);
SomeDirective = tslib_1.__decorate([
Directive({ selector: '[someDirective]' }),
tslib_1.__param(2, Inject(INJECTED_TOKEN)),
], SomeDirective);
})();
export { SomeDirective };
`,
},
{
name: _('/node_modules/@angular/core/some_directive.js'),
Expand Down Expand Up @@ -203,6 +233,27 @@ runInEachFileSystem(() => {
]);
});

it('should find the decorators on an IIFE wrapped class when mixing `ctorParameters` and `__decorate`',
() => {
const bundle = makeTestBundleProgram(_('/some_directive_ctor_parameters_iife.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle);
const classNode = getDeclaration(
bundle.program, _('/some_directive_ctor_parameters_iife.js'), 'SomeDirective',
isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode)!;

expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);

const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.identifier!.getText()).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args!.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});

it('should support decorators being used inside @angular/core', () => {
const bundle =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
Expand Down Expand Up @@ -260,6 +311,21 @@ runInEachFileSystem(() => {
expect(input1.decorators!.map(d => d.name)).toEqual(['Input']);
});

it('should find decorated members on an IIFE wrapped class when mixing `ctorParameters` and `__decorate`',
() => {
const bundle = makeTestBundleProgram(_('/some_directive_ctor_parameters_iife.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle);
const classNode = getDeclaration(
bundle.program, _('/some_directive_ctor_parameters_iife.js'), 'SomeDirective',
isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);

const input1 = members.find(member => member.name === 'input1')!;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators!.map(d => d.name)).toEqual(['Input']);
});

it('should find non decorated properties on a class', () => {
const bundle = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle);
Expand Down Expand Up @@ -383,6 +449,32 @@ runInEachFileSystem(() => {
});
});

it('should find the decorated constructor parameters on an IIFE wrapped class when mixing `ctorParameters` and `__decorate`',
() => {
const bundle = makeTestBundleProgram(_('/some_directive_ctor_parameters_iife.js'));
const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle);
const classNode = getDeclaration(
bundle.program, _('/some_directive_ctor_parameters_iife.js'), 'SomeDirective',
isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);

expect(parameters).toBeDefined();
expect(parameters!.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters!, [
'ViewContainerRef',
'TemplateRef',
null,
]);

const decorators = parameters![2].decorators!;
expect(decorators.length).toEqual(1);
expect(decorators[0].name).toBe('Inject');
expect(decorators[0].import!.from).toBe('@angular/core');
expect(decorators[0].import!.name).toBe('Inject');
});

describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const bundle = makeTestBundleProgram(_('/some_directive.js'));
Expand Down

0 comments on commit 2cb3b66

Please sign in to comment.