fix(ivy): support tsutils.__decorate
decorator declarations in ngcc (#26236)
The most recent Angular distributions have begun to use __decorate instead of Class.decorators. This prevents `ngcc` from recognizing the classes and then fails to perform the transform to ivy format. Example: ``` var ApplicationModule = /** @class */ (function () { // Inject ApplicationRef to make it eager... function ApplicationModule(appRef) { } ApplicationModule = __decorate([ NgModule({ providers: APPLICATION_MODULE_PROVIDERS }), __metadata("design:paramtypes", [ApplicationRef]) ], ApplicationModule); return ApplicationModule; }()); ``` Now `ngcc` recognizes `__decorate([...])` declarations and performs its transform. See FW-379 PR Close #26236
This commit is contained in:

committed by
Jason Aden

parent
13cdd13511
commit
7d08722e80
@ -11,7 +11,8 @@ import {makeProgram as _makeProgram} from '../../../ngtsc/testing/in_memory_type
|
||||
export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript';
|
||||
|
||||
export function makeProgram(...files: {name: string, contents: string}[]): ts.Program {
|
||||
return _makeProgram([getFakeCore(), ...files], {allowJs: true, checkJs: false}).program;
|
||||
return _makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false})
|
||||
.program;
|
||||
}
|
||||
|
||||
// TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package
|
||||
@ -51,6 +52,17 @@ export function getFakeCore() {
|
||||
};
|
||||
}
|
||||
|
||||
export function getFakeTslib() {
|
||||
return {
|
||||
name: 'node_modules/tslib/index.ts',
|
||||
contents: `
|
||||
export function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any) {}
|
||||
export function __param(paramIndex: number, decorator: any) {}
|
||||
export function __metadata(metadataKey: any, metadataValue: any) {}
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
export function convertToDirectTsLibImport(filesystem: {name: string, contents: string}[]) {
|
||||
return filesystem.map(file => {
|
||||
const contents =
|
||||
@ -58,7 +70,7 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents:
|
||||
.replace(
|
||||
`import * as tslib_1 from 'tslib';`,
|
||||
`import { __decorate, __metadata, __read, __values, __param, __extends, __assign } from 'tslib';`)
|
||||
.replace('tslib_1.', '');
|
||||
.replace(/tslib_1\./g, '');
|
||||
return {...file, contents};
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,396 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassMemberKind, Import} from '../../../ngtsc/host';
|
||||
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
|
||||
import {convertToDirectTsLibImport, getDeclaration, makeProgram} from '../helpers/utils';
|
||||
|
||||
const FILES = [
|
||||
{
|
||||
name: '/some_directive.js',
|
||||
contents: `
|
||||
import * as tslib_1 from "tslib";
|
||||
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
|
||||
var INJECTED_TOKEN = new InjectionToken('injected');
|
||||
var ViewContainerRef = /** @class */ (function () {
|
||||
function ViewContainerRef() {
|
||||
}
|
||||
return ViewContainerRef;
|
||||
}());
|
||||
var TemplateRef = /** @class */ (function () {
|
||||
function TemplateRef() {
|
||||
}
|
||||
return TemplateRef;
|
||||
}());
|
||||
var SomeDirective = /** @class */ (function () {
|
||||
function SomeDirective(_viewContainer, _template, injected) {
|
||||
this.instanceProperty = 'instance';
|
||||
this.input1 = '';
|
||||
this.input2 = 0;
|
||||
}
|
||||
SomeDirective.prototype.instanceMethod = function () { };
|
||||
SomeDirective.staticMethod = function () { };
|
||||
SomeDirective.staticProperty = 'static';
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", String)
|
||||
], SomeDirective.prototype, "input1", void 0);
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", Number)
|
||||
], SomeDirective.prototype, "input2", void 0);
|
||||
SomeDirective = tslib_1.__decorate([
|
||||
Directive({ selector: '[someDirective]' }),
|
||||
tslib_1.__param(2, Inject(INJECTED_TOKEN)),
|
||||
tslib_1.__metadata("design:paramtypes", [ViewContainerRef,
|
||||
TemplateRef, String])
|
||||
], SomeDirective);
|
||||
return SomeDirective;
|
||||
}());
|
||||
export { SomeDirective };
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '/node_modules/@angular/core/some_directive.js',
|
||||
contents: `
|
||||
import * as tslib_1 from "tslib";
|
||||
import { Directive, Input } from './directives';
|
||||
var SomeDirective = /** @class */ (function () {
|
||||
function SomeDirective() {
|
||||
this.input1 = '';
|
||||
}
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", String)
|
||||
], SomeDirective.prototype, "input1", void 0);
|
||||
SomeDirective = tslib_1.__decorate([
|
||||
Directive({ selector: '[someDirective]' }),
|
||||
], SomeDirective);
|
||||
return SomeDirective;
|
||||
}());
|
||||
export { SomeDirective };
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '/ngmodule.js',
|
||||
contents: `
|
||||
import * as tslib_1 from "tslib";
|
||||
import { NgModule } from '@angular/core';
|
||||
var HttpClientXsrfModule = /** @class */ (function () {
|
||||
function HttpClientXsrfModule() {
|
||||
}
|
||||
HttpClientXsrfModule_1 = HttpClientXsrfModule;
|
||||
HttpClientXsrfModule.withOptions = function (options) {
|
||||
if (options === void 0) { options = {}; }
|
||||
return {
|
||||
ngModule: HttpClientXsrfModule_1,
|
||||
providers: [],
|
||||
};
|
||||
};
|
||||
var HttpClientXsrfModule_1;
|
||||
HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
|
||||
NgModule({
|
||||
providers: [],
|
||||
})
|
||||
], HttpClientXsrfModule);
|
||||
return HttpClientXsrfModule;
|
||||
}());
|
||||
var missingValue;
|
||||
var nonDecoratedVar;
|
||||
nonDecoratedVar = 43;
|
||||
export { HttpClientXsrfModule };
|
||||
`
|
||||
},
|
||||
];
|
||||
|
||||
describe('Esm5ReflectionHost [import helper style]', () => {
|
||||
[{files: FILES, label: 'namespaced'},
|
||||
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
|
||||
].forEach(fileSystem => {
|
||||
describe(`[${fileSystem.label}]`, () => {
|
||||
|
||||
describe('getDecoratorsOfDeclaration()', () => {
|
||||
it('should find the decorators on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators).toBeDefined();
|
||||
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 use `getImportOfIdentifier()` to retrieve import info', () => {
|
||||
const mockImportInfo = {} as Import;
|
||||
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
|
||||
.and.callFake(
|
||||
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
|
||||
{from: '@angular/core', name: 'Directive'} :
|
||||
{});
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators.length).toEqual(1);
|
||||
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
|
||||
|
||||
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
|
||||
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support decorators being used inside @angular/core', () => {
|
||||
const program = makeProgram(fileSystem.files[1]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
|
||||
ts.isVariableDeclaration);
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators).toBeDefined();
|
||||
expect(decorators.length).toEqual(1);
|
||||
|
||||
const decorator = decorators[0];
|
||||
expect(decorator.name).toEqual('Directive');
|
||||
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
|
||||
expect(decorator.args !.map(arg => arg.getText())).toEqual([
|
||||
'{ selector: \'[someDirective]\' }',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembersOfClass()', () => {
|
||||
it('should find decorated members on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
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']);
|
||||
|
||||
const input2 = members.find(member => member.name === 'input2') !;
|
||||
expect(input2.kind).toEqual(ClassMemberKind.Property);
|
||||
expect(input2.isStatic).toEqual(false);
|
||||
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
|
||||
});
|
||||
|
||||
it('should find non decorated properties on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const members = host.getMembersOfClass(classNode);
|
||||
|
||||
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
|
||||
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
|
||||
expect(instanceProperty.isStatic).toEqual(false);
|
||||
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
|
||||
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
|
||||
});
|
||||
|
||||
it('should find static methods on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const members = host.getMembersOfClass(classNode);
|
||||
|
||||
const staticMethod = members.find(member => member.name === 'staticMethod') !;
|
||||
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
|
||||
expect(staticMethod.isStatic).toEqual(true);
|
||||
expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should find static properties on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
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(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
|
||||
host.getMembersOfClass(classNode);
|
||||
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
|
||||
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support decorators being used inside @angular/core', () => {
|
||||
const program = makeProgram(fileSystem.files[1]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
|
||||
ts.isVariableDeclaration);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstructorParameters', () => {
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const parameters = host.getConstructorParameters(classNode);
|
||||
|
||||
expect(parameters).toBeDefined();
|
||||
expect(parameters !.map(parameter => parameter.name)).toEqual([
|
||||
'_viewContainer', '_template', 'injected'
|
||||
]);
|
||||
expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([
|
||||
'ViewContainerRef', 'TemplateRef', 'String'
|
||||
]);
|
||||
});
|
||||
|
||||
describe('(returned parameters `decorators`)', () => {
|
||||
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
|
||||
const mockImportInfo = {} as Import;
|
||||
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
|
||||
.and.returnValue(mockImportInfo);
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const parameters = host.getConstructorParameters(classNode);
|
||||
const decorators = parameters ![2].decorators !;
|
||||
|
||||
expect(decorators.length).toEqual(1);
|
||||
expect(decorators[0].import).toBe(mockImportInfo);
|
||||
|
||||
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
|
||||
expect(typeIdentifier.text).toBe('Inject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeclarationOfIdentifier', () => {
|
||||
it('should return the declaration of a locally defined identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode) !;
|
||||
const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier;
|
||||
|
||||
const expectedDeclarationNode = getDeclaration(
|
||||
program, '/some_directive.js', 'ViewContainerRef', ts.isVariableDeclaration);
|
||||
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
|
||||
expect(actualDeclaration).not.toBe(null);
|
||||
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
|
||||
expect(actualDeclaration !.viaModule).toBe(null);
|
||||
});
|
||||
|
||||
it('should return the declaration of an externally defined identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
const decoratorNode = classDecorators[0].node;
|
||||
|
||||
const identifierOfDirective =
|
||||
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
|
||||
decoratorNode.expression :
|
||||
null;
|
||||
|
||||
const expectedDeclarationNode = getDeclaration(
|
||||
program, 'node_modules/@angular/core/index.ts', 'Directive',
|
||||
ts.isVariableDeclaration);
|
||||
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
|
||||
expect(actualDeclaration).not.toBe(null);
|
||||
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
|
||||
expect(actualDeclaration !.viaModule).toBe('@angular/core');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableValue', () => {
|
||||
it('should find the "actual" declaration of an aliased variable identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const ngModuleRef = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
|
||||
|
||||
const value = host.getVariableValue(ngModuleRef !);
|
||||
expect(value).not.toBe(null);
|
||||
if (!value || !ts.isFunctionDeclaration(value.parent)) {
|
||||
throw new Error(
|
||||
`Expected result to be a function declaration: ${value && value.getText()}.`);
|
||||
}
|
||||
expect(value.getText()).toBe('HttpClientXsrfModule');
|
||||
});
|
||||
|
||||
it('should return undefined if the variable has no assignment', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const missingValue = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
|
||||
const value = host.getVariableValue(missingValue !);
|
||||
expect(value).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if the variable is not assigned from a call to __decorate', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const nonDecoratedVar = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
|
||||
const value = host.getVariableValue(nonDecoratedVar !);
|
||||
expect(value).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function findVariableDeclaration(
|
||||
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
|
||||
node.name.text === variableName) {
|
||||
return node;
|
||||
}
|
||||
return node.forEachChild(node => findVariableDeclaration(node, variableName));
|
||||
}
|
||||
});
|
@ -1164,18 +1164,20 @@ describe('Esm5ReflectionHost', () => {
|
||||
expect(host.getClassSymbol(innerNode)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return the same class symbol for outer and inner declarations', () => {
|
||||
const program = makeProgram(SIMPLE_CLASS_FILE);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const outerNode =
|
||||
getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
const innerNode =
|
||||
(((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression)
|
||||
.expression as ts.FunctionExpression)
|
||||
.body.statements.find(ts.isFunctionDeclaration) !;
|
||||
it('should return the same class symbol (of the inner declaration) for outer and inner declarations',
|
||||
() => {
|
||||
const program = makeProgram(SIMPLE_CLASS_FILE);
|
||||
const host = new Esm5ReflectionHost(program.getTypeChecker());
|
||||
const outerNode = getDeclaration(
|
||||
program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration);
|
||||
const innerNode = (((outerNode.initializer as ts.ParenthesizedExpression)
|
||||
.expression as ts.CallExpression)
|
||||
.expression as ts.FunctionExpression)
|
||||
.body.statements.find(ts.isFunctionDeclaration) !;
|
||||
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
});
|
||||
expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode));
|
||||
expect(host.getClassSymbol(innerNode) !.valueDeclaration).toBe(innerNode);
|
||||
});
|
||||
|
||||
it('should return undefined if node is not an ES5 class', () => {
|
||||
const program = makeProgram(FOO_FUNCTION_FILE);
|
||||
|
@ -0,0 +1,379 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassMemberKind, Import} from '../../../ngtsc/host';
|
||||
import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host';
|
||||
import {convertToDirectTsLibImport, getDeclaration, makeProgram} from '../helpers/utils';
|
||||
|
||||
const FILES = [
|
||||
{
|
||||
name: '/some_directive.js',
|
||||
contents: `
|
||||
import * as tslib_1 from 'tslib';
|
||||
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
|
||||
const INJECTED_TOKEN = new InjectionToken('injected');
|
||||
class ViewContainerRef {
|
||||
}
|
||||
class TemplateRef {
|
||||
}
|
||||
let SomeDirective = class SomeDirective {
|
||||
constructor(_viewContainer, _template, injected) {
|
||||
this.instanceProperty = 'instance';
|
||||
this.input1 = '';
|
||||
this.input2 = 0;
|
||||
}
|
||||
instanceMethod() { }
|
||||
static staticMethod() { }
|
||||
};
|
||||
SomeDirective.staticProperty = 'static';
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", String)
|
||||
], SomeDirective.prototype, "input1", void 0);
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", Number)
|
||||
], SomeDirective.prototype, "input2", void 0);
|
||||
SomeDirective = tslib_1.__decorate([
|
||||
Directive({ selector: '[someDirective]' }),
|
||||
tslib_1.__param(2, Inject(INJECTED_TOKEN)),
|
||||
tslib_1.__metadata("design:paramtypes", [ViewContainerRef,
|
||||
TemplateRef, String])
|
||||
], SomeDirective);
|
||||
export { SomeDirective };
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '/node_modules/@angular/core/some_directive.js',
|
||||
contents: `
|
||||
import * as tslib_1 from 'tslib';
|
||||
import { Directive, Input } from './directives';
|
||||
let SomeDirective = class SomeDirective {
|
||||
constructor() { this.input1 = ''; }
|
||||
};
|
||||
tslib_1.__decorate([
|
||||
Input(),
|
||||
tslib_1.__metadata("design:type", String)
|
||||
], SomeDirective.prototype, "input1", void 0);
|
||||
SomeDirective = tslib_1.__decorate([
|
||||
Directive({ selector: '[someDirective]' }),
|
||||
], SomeDirective);
|
||||
export { SomeDirective };
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'ngmodule.js',
|
||||
contents: `
|
||||
import * as tslib_1 from 'tslib';
|
||||
import { NgModule } from './directives';
|
||||
var HttpClientXsrfModule_1;
|
||||
let HttpClientXsrfModule = HttpClientXsrfModule_1 = class HttpClientXsrfModule {
|
||||
static withOptions(options = {}) {
|
||||
return {
|
||||
ngModule: HttpClientXsrfModule_1,
|
||||
providers: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
|
||||
NgModule({
|
||||
providers: [],
|
||||
})
|
||||
], HttpClientXsrfModule);
|
||||
let missingValue;
|
||||
let nonDecoratedVar;
|
||||
nonDecoratedVar = 43;
|
||||
export { HttpClientXsrfModule };
|
||||
`
|
||||
},
|
||||
];
|
||||
|
||||
describe('Fesm2015ReflectionHost [import helper style]', () => {
|
||||
[{files: FILES, label: 'namespaced'},
|
||||
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
|
||||
].forEach(fileSystem => {
|
||||
describe(`[${fileSystem.label}]`, () => {
|
||||
|
||||
describe('getDecoratorsOfDeclaration()', () => {
|
||||
it('should find the decorators on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators).toBeDefined();
|
||||
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 use `getImportOfIdentifier()` to retrieve import info', () => {
|
||||
const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier')
|
||||
.and.callFake(
|
||||
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
|
||||
{from: '@angular/core', name: 'Directive'} :
|
||||
{});
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators.length).toEqual(1);
|
||||
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
|
||||
|
||||
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
|
||||
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support decorators being used inside @angular/core', () => {
|
||||
const program = makeProgram(fileSystem.files[1]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
|
||||
ts.isVariableDeclaration);
|
||||
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
|
||||
expect(decorators).toBeDefined();
|
||||
expect(decorators.length).toEqual(1);
|
||||
|
||||
const decorator = decorators[0];
|
||||
expect(decorator.name).toEqual('Directive');
|
||||
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
|
||||
expect(decorator.args !.map(arg => arg.getText())).toEqual([
|
||||
'{ selector: \'[someDirective]\' }',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembersOfClass()', () => {
|
||||
it('should find decorated members on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
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']);
|
||||
|
||||
const input2 = members.find(member => member.name === 'input2') !;
|
||||
expect(input2.kind).toEqual(ClassMemberKind.Property);
|
||||
expect(input2.isStatic).toEqual(false);
|
||||
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
|
||||
});
|
||||
|
||||
it('should find non decorated properties on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const members = host.getMembersOfClass(classNode);
|
||||
|
||||
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
|
||||
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
|
||||
expect(instanceProperty.isStatic).toEqual(false);
|
||||
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
|
||||
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
|
||||
});
|
||||
|
||||
it('should find static methods on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const members = host.getMembersOfClass(classNode);
|
||||
|
||||
const staticMethod = members.find(member => member.name === 'staticMethod') !;
|
||||
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
|
||||
expect(staticMethod.isStatic).toEqual(true);
|
||||
expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should find static properties on a class', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
|
||||
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(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
|
||||
host.getMembersOfClass(classNode);
|
||||
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
|
||||
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support decorators being used inside @angular/core', () => {
|
||||
const program = makeProgram(fileSystem.files[1]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
|
||||
ts.isVariableDeclaration);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstructorParameters', () => {
|
||||
it('should find the decorated constructor parameters', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const parameters = host.getConstructorParameters(classNode);
|
||||
|
||||
expect(parameters).toBeDefined();
|
||||
expect(parameters !.map(parameter => parameter.name)).toEqual([
|
||||
'_viewContainer', '_template', 'injected'
|
||||
]);
|
||||
expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([
|
||||
'ViewContainerRef', 'TemplateRef', 'String'
|
||||
]);
|
||||
});
|
||||
|
||||
describe('(returned parameters `decorators`)', () => {
|
||||
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
|
||||
const mockImportInfo = {} as Import;
|
||||
const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier')
|
||||
.and.returnValue(mockImportInfo);
|
||||
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const parameters = host.getConstructorParameters(classNode);
|
||||
const decorators = parameters ![2].decorators !;
|
||||
|
||||
expect(decorators.length).toEqual(1);
|
||||
expect(decorators[0].import).toBe(mockImportInfo);
|
||||
|
||||
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
|
||||
expect(typeIdentifier.text).toBe('Inject');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeclarationOfIdentifier', () => {
|
||||
it('should return the declaration of a locally defined identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const ctrDecorators = host.getConstructorParameters(classNode) !;
|
||||
const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier;
|
||||
|
||||
const expectedDeclarationNode = getDeclaration(
|
||||
program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration);
|
||||
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
|
||||
expect(actualDeclaration).not.toBe(null);
|
||||
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
|
||||
expect(actualDeclaration !.viaModule).toBe(null);
|
||||
});
|
||||
|
||||
it('should return the declaration of an externally defined identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[0]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const classNode = getDeclaration(
|
||||
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
|
||||
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
|
||||
const decoratorNode = classDecorators[0].node;
|
||||
const identifierOfDirective =
|
||||
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
|
||||
decoratorNode.expression :
|
||||
null;
|
||||
|
||||
const expectedDeclarationNode = getDeclaration(
|
||||
program, 'node_modules/@angular/core/index.ts', 'Directive',
|
||||
ts.isVariableDeclaration);
|
||||
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
|
||||
expect(actualDeclaration).not.toBe(null);
|
||||
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
|
||||
expect(actualDeclaration !.viaModule).toBe('@angular/core');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableValue', () => {
|
||||
it('should find the "actual" declaration of an aliased variable identifier', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const ngModuleRef = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
|
||||
|
||||
const value = host.getVariableValue(ngModuleRef !);
|
||||
expect(value).not.toBe(null);
|
||||
if (!value || !ts.isClassExpression(value)) {
|
||||
throw new Error(
|
||||
`Expected value to be a class expression: ${value && value.getText()}.`);
|
||||
}
|
||||
expect(value.name !.text).toBe('HttpClientXsrfModule');
|
||||
});
|
||||
|
||||
it('should return null if the variable has no assignment', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const missingValue = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
|
||||
const value = host.getVariableValue(missingValue !);
|
||||
expect(value).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if the variable is not assigned from a call to __decorate', () => {
|
||||
const program = makeProgram(fileSystem.files[2]);
|
||||
const host = new Fesm2015ReflectionHost(program.getTypeChecker());
|
||||
const nonDecoratedVar = findVariableDeclaration(
|
||||
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
|
||||
const value = host.getVariableValue(nonDecoratedVar !);
|
||||
expect(value).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function findVariableDeclaration(
|
||||
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
|
||||
node.name.text === variableName) {
|
||||
return node;
|
||||
}
|
||||
return node.forEachChild(node => findVariableDeclaration(node, variableName));
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user