diff --git a/packages/ngtools/webpack/src/transformers/ctor-parameters.ts b/packages/ngtools/webpack/src/transformers/ctor-parameters.ts index 6adc5d88bedd..9e456b170200 100644 --- a/packages/ngtools/webpack/src/transformers/ctor-parameters.ts +++ b/packages/ngtools/webpack/src/transformers/ctor-parameters.ts @@ -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[] = []; @@ -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')), @@ -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)) { @@ -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); @@ -263,6 +279,7 @@ export function decoratorDownlevelTransformer( diagnostics, entityNameToExpression, parametersInfo, + typeChecker, ); return [node, ctorProperty]; diff --git a/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts b/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts new file mode 100644 index 000000000000..c9425666da10 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/ctor-parameters_spec.ts @@ -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) { + 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('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('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('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}`); + }); +});