Skip to content

Commit a7ca658

Browse files
crisbetoatscott
authored andcommitted
fix(ivy): handle overloaded constructors in ngtsc (#34590)
Currently ngtsc looks for the first `ConstructorDeclaration` when figuring out what the parameters are so that it can generate the DI instructions. The problem is that if a constructor has overloads, it'll have several `ConstructorDeclaration` members with a different number of parameters. These changes tweak the logic so it looks for the constructor implementation. PR Close #34590
1 parent 3113bb7 commit a7ca658

File tree

5 files changed

+115
-3
lines changed

5 files changed

+115
-3
lines changed

packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ export class TypeScriptReflectionHost implements ReflectionHost {
3535
getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null {
3636
const tsClazz = castDeclarationToClassOrDie(clazz);
3737

38-
// First, find the constructor.
39-
const ctor = tsClazz.members.find(ts.isConstructorDeclaration);
38+
// First, find the constructor with a `body`. The constructors without a `body` are overloads
39+
// whereas we want the implementation since it's the one that'll be executed and which can
40+
// have decorators.
41+
const ctor = tsClazz.members.find(
42+
(member): member is ts.ConstructorDeclaration =>
43+
ts.isConstructorDeclaration(member) && member.body !== undefined);
4044
if (ctor === undefined) {
4145
return null;
4246
}
@@ -564,4 +568,4 @@ function getExportedName(decl: ts.Declaration, originalId: ts.Identifier): strin
564568
return ts.isImportSpecifier(decl) ?
565569
(decl.propertyName !== undefined ? decl.propertyName : decl.name).text :
566570
originalId.text;
567-
}
571+
}

packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,29 @@ runInEachFileSystem(() => {
211211
expect(args.length).toBe(1);
212212
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
213213
});
214+
215+
it('should reflect the arguments from an overloaded constructor', () => {
216+
const {program} = makeProgram([{
217+
name: _('/entry.ts'),
218+
contents: `
219+
class Bar {}
220+
class Baz {}
221+
222+
class Foo {
223+
constructor(bar: Bar);
224+
constructor(bar: Bar, baz?: Baz) {}
225+
}
226+
`
227+
}]);
228+
const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration);
229+
const checker = program.getTypeChecker();
230+
const host = new TypeScriptReflectionHost(checker);
231+
const args = host.getConstructorParameters(clazz) !;
232+
expect(args.length).toBe(2);
233+
expectParameter(args[0], 'bar', 'Bar');
234+
expectParameter(args[1], 'baz', 'Baz');
235+
});
236+
214237
});
215238

216239

packages/compiler-cli/test/compliance/r3_view_compiler_di_spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,41 @@ describe('compiler compliance: dependency injection', () => {
9999
expectEmit(result.source, def, 'Incorrect injectable definition');
100100
});
101101

102+
it('should create a factory definition for an injectable with an overloaded constructor', () => {
103+
const files = {
104+
app: {
105+
'spec.ts': `
106+
import {Injectable, Optional} from '@angular/core';
107+
108+
class MyDependency {}
109+
class MyOptionalDependency {}
110+
111+
@Injectable()
112+
export class MyService {
113+
constructor(dep: MyDependency);
114+
constructor(dep: MyDependency, @Optional() optionalDep?: MyOptionalDependency) {}
115+
}
116+
`
117+
}
118+
};
119+
120+
const factory = `
121+
MyService.ɵfac = function MyService_Factory(t) {
122+
return new (t || MyService)($r3$.ɵɵinject(MyDependency), $r3$.ɵɵinject(MyOptionalDependency, 8));
123+
}`;
124+
125+
const def = `
126+
MyService.ɵprov = $r3$.ɵɵdefineInjectable({
127+
token: MyService,
128+
factory: MyService.ɵfac
129+
});
130+
`;
131+
132+
const result = compile(files, angularFiles);
133+
expectEmit(result.source, factory, 'Incorrect factory definition');
134+
expectEmit(result.source, def, 'Incorrect injectable definition');
135+
});
136+
102137
it('should create a single factory def if the class has more than one decorator', () => {
103138
const files = {
104139
app: {

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,32 @@ runInEachFileSystem(os => {
192192
expect(jsContents).toContain('inject(Dep, 8)');
193193
});
194194

195+
it('should compile @Injectable with constructor overloads', () => {
196+
env.write('test.ts', `
197+
import {Injectable, Optional} from '@angular/core';
198+
199+
@Injectable()
200+
class Dep {}
201+
202+
@Injectable()
203+
class OptionalDep {}
204+
205+
@Injectable()
206+
class Service {
207+
constructor(dep: Dep);
208+
209+
constructor(dep: Dep, @Optional() optionalDep?: OptionalDep) {}
210+
}
211+
`);
212+
env.driveMain();
213+
const jsContents = env.getContents('test.js');
214+
215+
expect(jsContents)
216+
.toContain(
217+
`Service.ɵfac = function Service_Factory(t) { ` +
218+
`return new (t || Service)(i0.ɵɵinject(Dep), i0.ɵɵinject(OptionalDep, 8)); };`);
219+
});
220+
195221
it('should compile Directives without errors', () => {
196222
env.write('test.ts', `
197223
import {Directive} from '@angular/core';

packages/core/test/acceptance/di_spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,30 @@ describe('di', () => {
982982
`This will become an error in v10. Please add @Injectable() to the "MyRootService" class.`);
983983
}
984984
});
985+
986+
it('should inject services in constructor with overloads', () => {
987+
@Injectable({providedIn: 'root'})
988+
class MyService {
989+
}
990+
991+
@Injectable({providedIn: 'root'})
992+
class MyOtherService {
993+
}
994+
995+
@Component({template: ''})
996+
class MyComp {
997+
constructor(myService: MyService);
998+
constructor(
999+
public myService: MyService, @Optional() public myOtherService?: MyOtherService) {}
1000+
}
1001+
TestBed.configureTestingModule({declarations: [MyComp]});
1002+
const fixture = TestBed.createComponent(MyComp);
1003+
fixture.detectChanges();
1004+
1005+
expect(fixture.componentInstance.myService instanceof MyService).toBe(true);
1006+
expect(fixture.componentInstance.myOtherService instanceof MyOtherService).toBe(true);
1007+
});
1008+
9851009
});
9861010

9871011
describe('inject', () => {

0 commit comments

Comments
 (0)