Skip to content

Commit

Permalink
fix(@ngtools/webpack): skip non-runtime types when transforming const…
Browse files Browse the repository at this point in the history
…ructors

Fixes angular#14876
  • Loading branch information
clydin committed Jun 25, 2019
1 parent ce578ae commit 5fd978c
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 1 deletion.
19 changes: 18 additions & 1 deletion packages/ngtools/webpack/src/transformers/ctor-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function createCtorParametersClassProperty(
diagnostics: ts.Diagnostic[],
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
ctorParameters: ParameterDecorationInfo[],
typeChecker: ts.TypeChecker,
): ts.PropertyDeclaration {
const params: ts.Expression[] = [];

Expand All @@ -99,7 +100,7 @@ function createCtorParametersClassProperty(
}

const paramType = ctorParam.type
? typeReferenceToExpression(entityNameToExpression, ctorParam.type)
? typeReferenceToExpression(entityNameToExpression, ctorParam.type, typeChecker)
: undefined;
const members = [
ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined')),
Expand Down Expand Up @@ -147,6 +148,7 @@ function createCtorParametersClassProperty(
function typeReferenceToExpression(
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
node: ts.TypeNode,
typeChecker: ts.TypeChecker,
): ts.Expression | undefined {
let kind = node.kind;
if (ts.isLiteralTypeNode(node)) {
Expand Down Expand Up @@ -175,6 +177,20 @@ function typeReferenceToExpression(
return ts.createIdentifier('Number');
case ts.SyntaxKind.TypeReference:
const typeRef = node as ts.TypeReferenceNode;
debugger;
let typeSymbol = typeChecker.getSymbolAtLocation(typeRef.typeName);
if (typeSymbol && typeSymbol.flags & ts.SymbolFlags.Alias) {
typeSymbol = typeChecker.getAliasedSymbol(typeSymbol);
}

if (!typeSymbol || !(typeSymbol.flags & ts.SymbolFlags.Value)) {
return undefined;
}

const type = typeChecker.getTypeOfSymbolAtLocation(typeSymbol, typeRef);
if (!type || typeChecker.getSignaturesOfType(type, ts.SignatureKind.Construct).length === 0) {
return undefined;
}

// Ignore any generic types, just return the base type.
return entityNameToExpression(typeRef.typeName);
Expand Down Expand Up @@ -263,6 +279,7 @@ export function decoratorDownlevelTransformer(
diagnostics,
entityNameToExpression,
parametersInfo,
typeChecker,
);

return [node, ctorProperty];
Expand Down
174 changes: 174 additions & 0 deletions packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @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 { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies
import { createTypescriptContext, transformTypescript } from './ast_helpers';
import { downlevelConstructorParameters } from './ctor-parameters';

function transform(input: string, additionalFiles?: Record<string, string>) {
const { program, compilerHost } = createTypescriptContext(input, additionalFiles);
const transformer = downlevelConstructorParameters(() => program.getTypeChecker());
const result = transformTypescript(undefined, [transformer], program, compilerHost);

return result;
}

describe('Constructor Parameter Transformer', () => {
it('records class name in same module', () => {
const input = `
export class ClassInject {};
export class MyService {
constructor(v: ClassInject) {}
}
`;

const output = `
export class ClassInject { } ;
export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: ClassInject } ];
`;

const result = transform(input);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('records class name of root-provided injectable in same module', () => {
const input = `
@Injectable({
providedIn: 'root'
})
export class RootProvidedService {
constructor() { }
}
export class MyService {
constructor(v: RootProvidedService) {}
}
`;

const output = `
import * as tslib_1 from "tslib";
let RootProvidedService = class RootProvidedService { constructor() { } };
RootProvidedService = tslib_1.__decorate([ Injectable({ providedIn: 'root' }) ], RootProvidedService);
export { RootProvidedService }; export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: RootProvidedService } ];
`;

const result = transform(input);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

// The current testing infrastructure does not support this test
// Aliased TS symbols are resolved to 'unknown'
xit('records class name of root-provided injectable in imported module', () => {
const rootProvided = {
'root-provided-service': `
@Injectable({
providedIn: 'root'
})
export class RootProvidedService {
constructor() { }
}
`,
};

const input = `
import { RootProvidedService } from './root-provided-service';
export class MyService {
constructor(v: RootProvidedService) {}
}
`;

const output = `export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: RootProvidedService } ];`;

const result = transform(input, rootProvided);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('does not record exported interface name in same module with Inject decorators', () => {
const input = `
export interface InterInject {}
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
export class MyService {
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
}
`;

const output = `
import * as tslib_1 from "tslib";
export const INTERFACE_INJECT = new InjectionToken('interface-inject');
let MyService = class MyService { constructor(v) { } };
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
export { MyService };
`;

const result = transform(input);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('does not record interface name in same module with Inject decorators', () => {
const input = `
interface InterInject {}
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
export class MyService {
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
}
`;

const output = `
import * as tslib_1 from "tslib";
export const INTERFACE_INJECT = new InjectionToken('interface-inject');
let MyService = class MyService { constructor(v) { } };
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
export { MyService };
`;

const result = transform(input);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('does not record interface name in imported module with Inject decorators', () => {
const injectedModule = {
'module-inject': `
export interface InterInject {};
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
`,
};

const input = `
import { INTERFACE_INJECT, InterInject } from './module-inject';
export class MyService {
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
}
`;

const output = `
import * as tslib_1 from "tslib";
import { INTERFACE_INJECT } from './module-inject';
let MyService = class MyService { constructor(v) { } };
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
export { MyService };
`;

const result = transform(input, injectedModule);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});
});

0 comments on commit 5fd978c

Please sign in to comment.