Skip to content

Commit 17ad7d4

Browse files
clydinvikerman
authored andcommitted
fix(@ngtools/webpack): skip non-runtime types when transforming constructors
Fixes angular#14876
1 parent 6f24c4e commit 17ad7d4

File tree

2 files changed

+191
-1
lines changed

2 files changed

+191
-1
lines changed

src/transformers/ctor-parameters.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ function createCtorParametersClassProperty(
8989
diagnostics: ts.Diagnostic[],
9090
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
9191
ctorParameters: ParameterDecorationInfo[],
92+
typeChecker: ts.TypeChecker,
9293
): ts.PropertyDeclaration {
9394
const params: ts.Expression[] = [];
9495

@@ -99,7 +100,7 @@ function createCtorParametersClassProperty(
99100
}
100101

101102
const paramType = ctorParam.type
102-
? typeReferenceToExpression(entityNameToExpression, ctorParam.type)
103+
? typeReferenceToExpression(entityNameToExpression, ctorParam.type, typeChecker)
103104
: undefined;
104105
const members = [
105106
ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined')),
@@ -147,6 +148,7 @@ function createCtorParametersClassProperty(
147148
function typeReferenceToExpression(
148149
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
149150
node: ts.TypeNode,
151+
typeChecker: ts.TypeChecker,
150152
): ts.Expression | undefined {
151153
let kind = node.kind;
152154
if (ts.isLiteralTypeNode(node)) {
@@ -175,6 +177,19 @@ function typeReferenceToExpression(
175177
return ts.createIdentifier('Number');
176178
case ts.SyntaxKind.TypeReference:
177179
const typeRef = node as ts.TypeReferenceNode;
180+
let typeSymbol = typeChecker.getSymbolAtLocation(typeRef.typeName);
181+
if (typeSymbol && typeSymbol.flags & ts.SymbolFlags.Alias) {
182+
typeSymbol = typeChecker.getAliasedSymbol(typeSymbol);
183+
}
184+
185+
if (!typeSymbol || !(typeSymbol.flags & ts.SymbolFlags.Value)) {
186+
return undefined;
187+
}
188+
189+
const type = typeChecker.getTypeOfSymbolAtLocation(typeSymbol, typeRef);
190+
if (!type || typeChecker.getSignaturesOfType(type, ts.SignatureKind.Construct).length === 0) {
191+
return undefined;
192+
}
178193

179194
// Ignore any generic types, just return the base type.
180195
return entityNameToExpression(typeRef.typeName);
@@ -263,6 +278,7 @@ export function decoratorDownlevelTransformer(
263278
diagnostics,
264279
entityNameToExpression,
265280
parametersInfo,
281+
typeChecker,
266282
);
267283

268284
return [node, ctorProperty];
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies
9+
import { createTypescriptContext, transformTypescript } from './ast_helpers';
10+
import { downlevelConstructorParameters } from './ctor-parameters';
11+
12+
function transform(input: string, additionalFiles?: Record<string, string>) {
13+
const { program, compilerHost } = createTypescriptContext(input, additionalFiles);
14+
const transformer = downlevelConstructorParameters(() => program.getTypeChecker());
15+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
16+
17+
return result;
18+
}
19+
20+
describe('Constructor Parameter Transformer', () => {
21+
it('records class name in same module', () => {
22+
const input = `
23+
export class ClassInject {};
24+
25+
export class MyService {
26+
constructor(v: ClassInject) {}
27+
}
28+
`;
29+
30+
const output = `
31+
export class ClassInject { } ;
32+
export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: ClassInject } ];
33+
`;
34+
35+
const result = transform(input);
36+
37+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
38+
});
39+
40+
it('records class name of root-provided injectable in same module', () => {
41+
const input = `
42+
@Injectable({
43+
providedIn: 'root'
44+
})
45+
export class RootProvidedService {
46+
47+
constructor() { }
48+
}
49+
50+
export class MyService {
51+
constructor(v: RootProvidedService) {}
52+
}
53+
`;
54+
55+
const output = `
56+
import * as tslib_1 from "tslib";
57+
let RootProvidedService = class RootProvidedService { constructor() { } };
58+
RootProvidedService = tslib_1.__decorate([ Injectable({ providedIn: 'root' }) ], RootProvidedService);
59+
export { RootProvidedService }; export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: RootProvidedService } ];
60+
`;
61+
62+
const result = transform(input);
63+
64+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
65+
});
66+
67+
// The current testing infrastructure does not support this test
68+
// Aliased TS symbols are resolved to 'unknown'
69+
xit('records class name of root-provided injectable in imported module', () => {
70+
const rootProvided = {
71+
'root-provided-service': `
72+
@Injectable({
73+
providedIn: 'root'
74+
})
75+
export class RootProvidedService {
76+
77+
constructor() { }
78+
}
79+
`,
80+
};
81+
82+
const input = `
83+
import { RootProvidedService } from './root-provided-service';
84+
85+
export class MyService {
86+
constructor(v: RootProvidedService) {}
87+
}
88+
`;
89+
90+
const output = `export class MyService { constructor(v) { } } MyService.ctorParameters = () => [ { type: RootProvidedService } ];`;
91+
92+
const result = transform(input, rootProvided);
93+
94+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
95+
});
96+
97+
it('does not record exported interface name in same module with Inject decorators', () => {
98+
const input = `
99+
export interface InterInject {}
100+
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
101+
102+
export class MyService {
103+
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
104+
}
105+
`;
106+
107+
const output = `
108+
import * as tslib_1 from "tslib";
109+
export const INTERFACE_INJECT = new InjectionToken('interface-inject');
110+
let MyService = class MyService { constructor(v) { } };
111+
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
112+
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
113+
export { MyService };
114+
`;
115+
116+
const result = transform(input);
117+
118+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
119+
});
120+
121+
it('does not record interface name in same module with Inject decorators', () => {
122+
const input = `
123+
interface InterInject {}
124+
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
125+
126+
export class MyService {
127+
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
128+
}
129+
`;
130+
131+
const output = `
132+
import * as tslib_1 from "tslib";
133+
export const INTERFACE_INJECT = new InjectionToken('interface-inject');
134+
let MyService = class MyService { constructor(v) { } };
135+
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
136+
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
137+
export { MyService };
138+
`;
139+
140+
const result = transform(input);
141+
142+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
143+
});
144+
145+
it('does not record interface name in imported module with Inject decorators', () => {
146+
const injectedModule = {
147+
'module-inject': `
148+
export interface InterInject {};
149+
export const INTERFACE_INJECT = new InjectionToken<InterInject>('interface-inject');
150+
`,
151+
};
152+
153+
const input = `
154+
import { INTERFACE_INJECT, InterInject } from './module-inject';
155+
156+
export class MyService {
157+
constructor(@Inject(INTERFACE_INJECT) v: InterInject) {}
158+
}
159+
`;
160+
161+
const output = `
162+
import * as tslib_1 from "tslib";
163+
import { INTERFACE_INJECT } from './module-inject';
164+
let MyService = class MyService { constructor(v) { } };
165+
MyService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [INTERFACE_INJECT,] }] } ];
166+
MyService = tslib_1.__decorate([ tslib_1.__param(0, Inject(INTERFACE_INJECT)) ], MyService);
167+
export { MyService };
168+
`;
169+
170+
const result = transform(input, injectedModule);
171+
172+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
173+
});
174+
});

0 commit comments

Comments
 (0)