Skip to content

Commit b7fe0e9

Browse files
chuckjazZhicheng Wang
authored and
Zhicheng Wang
committed
fix(compiler): support interface types in injectable constuctors (angular#14894)
Fixes angular#12631
1 parent 9936340 commit b7fe0e9

File tree

9 files changed

+122
-12
lines changed

9 files changed

+122
-12
lines changed

packages/compiler-cli/integrationtest/src/basic.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Component, Inject, LOCALE_ID, TRANSLATIONS_FORMAT} from '@angular/core';
1010

11+
import {CUSTOM, Named} from './custom_token';
1112

1213
@Component({
1314
selector: 'basic',
@@ -21,7 +22,8 @@ export class BasicComp {
2122
ctxArr: any[] = [];
2223
constructor(
2324
@Inject(LOCALE_ID) public localeId: string,
24-
@Inject(TRANSLATIONS_FORMAT) public translationsFormat: string) {
25+
@Inject(TRANSLATIONS_FORMAT) public translationsFormat: string,
26+
@Inject(CUSTOM) public custom: Named) {
2527
this.ctxProp = 'initialValue';
2628
}
2729
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
9+
import {InjectionToken} from '@angular/core';
10+
11+
export interface Named { name: string; }
12+
13+
export const CUSTOM = new InjectionToken<Named>('CUSTOM');

packages/compiler-cli/integrationtest/src/module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {MultipleComponentsMyComp, NextComp} from './a/multiple_components';
2020
import {AnimateCmp} from './animate';
2121
import {BasicComp} from './basic';
2222
import {ComponentUsingThirdParty} from './comp_using_3rdp';
23+
import {CUSTOM, Named} from './custom_token';
2324
import {CompWithAnalyzeEntryComponentsProvider, CompWithEntryComponents} from './entry_components';
2425
import {CompConsumingEvents, CompUsingPipes, CompWithProviders, CompWithReferences, DirPublishingEvents, ModuleUsingCustomElements} from './features';
2526
import {CompUsingRootModuleDirectiveAndPipe, SomeDirectiveInRootModule, SomeLibModule, SomePipeInRootModule, SomeService} from './module_fixtures';
@@ -75,6 +76,7 @@ export const SERVER_ANIMATIONS_PROVIDERS: Provider[] = [{
7576
providers: [
7677
SomeService,
7778
SERVER_ANIMATIONS_PROVIDERS,
79+
{provide: CUSTOM, useValue: {name: 'some name'}},
7880
],
7981
entryComponents: [
8082
AnimateCmp,

packages/compiler/test/aot/static_reflector_spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,31 @@ describe('StaticReflector', () => {
726726
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
727727
.toEqual([]);
728728
});
729+
730+
it('should support constructor parameters with @Inject and an interface type', () => {
731+
const data = Object.create(DEFAULT_TEST_DATA);
732+
const file = '/tmp/src/inject_interface.ts';
733+
data[file] = `
734+
import {Injectable, Inject} from '@angular/core';
735+
import {F} from './f';
736+
737+
export interface InjectedInterface {
738+
739+
}
740+
741+
export class Token {}
742+
743+
@Injectable()
744+
export class SomeClass {
745+
constructor (@Inject(Token) injected: InjectedInterface, t: Token, @Inject(Token) f: F) {}
746+
}
747+
`;
748+
749+
init(data);
750+
751+
expect(reflector.parameters(reflector.getStaticSymbol(file, 'SomeClass'))[0].length)
752+
.toEqual(1);
753+
});
729754
});
730755

731756
});

packages/compiler/test/aot/static_symbol_resolver_spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,9 @@ export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
424424
filePath, this.data[filePath], ts.ScriptTarget.ES5, /* setParentNodes */ true);
425425
const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
426426
if (diagnostics && diagnostics.length) {
427-
throw Error(`Error encountered during parse of file ${filePath}`);
427+
const errors = diagnostics.map(d => `(${d.start}-${d.start+d.length}): ${d.messageText}`)
428+
.join('\n ');
429+
throw Error(`Error encountered during parse of file ${filePath}\n${errors}`);
428430
}
429431
return [this.collector.getMetadata(sf)];
430432
}

tools/@angular/tsc-wrapped/src/bundler.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import * as path from 'path';
99
import * as ts from 'typescript';
1010

1111
import {MetadataCollector} from './collector';
12+
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataArray, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMethodMetadata} from './schema';
1213

13-
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataArray, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMethodMetadata} from './schema';
1414

1515
// The character set used to produce private names.
1616
const PRIVATE_NAME_CHARS = [
@@ -268,6 +268,9 @@ export class MetadataBundler {
268268
if (isFunctionMetadata(value)) {
269269
return this.convertFunction(moduleName, value);
270270
}
271+
if (isInterfaceMetadata(value)) {
272+
return value;
273+
}
271274
return this.convertValue(moduleName, value);
272275
}
273276

tools/@angular/tsc-wrapped/src/collector.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import * as ts from 'typescript';
1010

1111
import {Evaluator, errorSymbol} from './evaluator';
12-
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
12+
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
1313
import {Symbols} from './symbols';
1414

1515
// In TypeScript 2.1 these flags moved
@@ -56,7 +56,8 @@ export class MetadataCollector {
5656
*/
5757
public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata {
5858
const locals = new Symbols(sourceFile);
59-
const nodeMap = new Map<MetadataValue|ClassMetadata|FunctionMetadata, ts.Node>();
59+
const nodeMap =
60+
new Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>();
6061
const evaluator = new Evaluator(locals, nodeMap, this.options);
6162
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
6263
let exports: ModuleExportMetadata[];
@@ -264,13 +265,14 @@ export class MetadataCollector {
264265
});
265266

266267
const isExportedIdentifier = (identifier: ts.Identifier) => exportMap.has(identifier.text);
267-
const isExported = (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) =>
268-
isExport(node) || isExportedIdentifier(node.name);
268+
const isExported =
269+
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration |
270+
ts.EnumDeclaration) => isExport(node) || isExportedIdentifier(node.name);
269271
const exportedIdentifierName = (identifier: ts.Identifier) =>
270272
exportMap.get(identifier.text) || identifier.text;
271273
const exportedName =
272-
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) =>
273-
exportedIdentifierName(node.name);
274+
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration |
275+
ts.EnumDeclaration) => exportedIdentifierName(node.name);
274276

275277

276278
// Predeclare classes and functions
@@ -290,6 +292,15 @@ export class MetadataCollector {
290292
}
291293
break;
292294

295+
case ts.SyntaxKind.InterfaceDeclaration:
296+
const interfaceDeclaration = <ts.InterfaceDeclaration>node;
297+
if (interfaceDeclaration.name) {
298+
const interfaceName = interfaceDeclaration.name.text;
299+
// All references to interfaces should be converted to references to `any`.
300+
locals.define(interfaceName, {__symbolic: 'reference', name: 'any'});
301+
}
302+
break;
303+
293304
case ts.SyntaxKind.FunctionDeclaration:
294305
const functionDeclaration = <ts.FunctionDeclaration>node;
295306
if (!isExported(functionDeclaration)) {
@@ -356,6 +367,14 @@ export class MetadataCollector {
356367
// Otherwise don't record metadata for the class.
357368
break;
358369

370+
case ts.SyntaxKind.InterfaceDeclaration:
371+
const interfaceDeclaration = <ts.InterfaceDeclaration>node;
372+
if (interfaceDeclaration.name && isExported(interfaceDeclaration)) {
373+
if (!metadata) metadata = {};
374+
metadata[exportedName(interfaceDeclaration)] = {__symbolic: 'interface'};
375+
}
376+
break;
377+
359378
case ts.SyntaxKind.FunctionDeclaration:
360379
// Record functions that return a single value. Record the parameter
361380
// names substitution will be performed by the StaticReflector.

tools/@angular/tsc-wrapped/src/schema.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
export const VERSION = 3;
1919

20-
export type MetadataEntry = ClassMetadata | FunctionMetadata | MetadataValue;
20+
export type MetadataEntry = ClassMetadata | InterfaceMetadata | FunctionMetadata | MetadataValue;
2121

2222
export interface ModuleMetadata {
2323
__symbolic: 'module';
@@ -47,6 +47,11 @@ export function isClassMetadata(value: any): value is ClassMetadata {
4747
return value && value.__symbolic === 'class';
4848
}
4949

50+
export interface InterfaceMetadata { __symbolic: 'interface'; }
51+
export function isInterfaceMetadata(value: any): value is InterfaceMetadata {
52+
return value && value.__symbolic === 'interface';
53+
}
54+
5055
export interface MetadataMap { [name: string]: MemberMetadata[]; }
5156

5257
export interface MemberMetadata {

tools/@angular/tsc-wrapped/test/collector.spec.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ describe('Collector', () => {
5050
'static-method-with-default.ts',
5151
'class-inheritance.ts',
5252
'class-inheritance-parent.ts',
53-
'class-inheritance-declarations.d.ts'
53+
'class-inheritance-declarations.d.ts',
54+
'interface-reference.ts'
5455
]);
5556
service = ts.createLanguageService(host, documentRegistry);
5657
program = service.getProgram();
@@ -60,11 +61,18 @@ describe('Collector', () => {
6061
it('should not have errors in test data', () => { expectValidSources(service, program); });
6162

6263
it('should return undefined for modules that have no metadata', () => {
63-
const sourceFile = program.getSourceFile('app/hero.ts');
64+
const sourceFile = program.getSourceFile('app/empty.ts');
6465
const metadata = collector.getMetadata(sourceFile);
6566
expect(metadata).toBeUndefined();
6667
});
6768

69+
it('should return an interface reference for interfaces', () => {
70+
const sourceFile = program.getSourceFile('app/hero.ts');
71+
const metadata = collector.getMetadata(sourceFile);
72+
expect(metadata).toEqual(
73+
{__symbolic: 'module', version: 3, metadata: {Hero: {__symbolic: 'interface'}}});
74+
});
75+
6876
it('should be able to collect a simple component\'s metadata', () => {
6977
const sourceFile = program.getSourceFile('app/hero-detail.component.ts');
7078
const metadata = collector.getMetadata(sourceFile);
@@ -609,6 +617,22 @@ describe('Collector', () => {
609617
});
610618
});
611619

620+
it('should collect any for interface parameter reference', () => {
621+
const source = program.getSourceFile('/interface-reference.ts');
622+
const metadata = collector.getMetadata(source);
623+
expect((metadata.metadata['SomeClass'] as ClassMetadata).members).toEqual({
624+
__ctor__: [{
625+
__symbolic: 'constructor',
626+
parameterDecorators: [[{
627+
__symbolic: 'call',
628+
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Inject'},
629+
arguments: ['a']
630+
}]],
631+
parameters: [{__symbolic: 'reference', name: 'any'}]
632+
}]
633+
});
634+
});
635+
612636
describe('in strict mode', () => {
613637
it('should throw if an error symbol is collecting a reference to a non-exported symbol', () => {
614638
const source = program.getSourceFile('/local-symbol-ref.ts');
@@ -759,6 +783,7 @@ const FILES: Directory = {
759783
id: number;
760784
name: string;
761785
}`,
786+
'empty.ts': ``,
762787
'hero-detail.component.ts': `
763788
import {Component, Input} from 'angular2/core';
764789
import {Hero} from './hero';
@@ -927,6 +952,15 @@ const FILES: Directory = {
927952
}
928953
}
929954
`,
955+
'interface-reference.ts': `
956+
import {Injectable, Inject} from 'angular2/core';
957+
export interface Test {}
958+
959+
@Injectable()
960+
export class SomeClass {
961+
constructor(@Inject("a") test: Test) {}
962+
}
963+
`,
930964
'import-star.ts': `
931965
import {Injectable} from 'angular2/core';
932966
import * as common from 'angular2/common';
@@ -1146,6 +1180,11 @@ const FILES: Directory = {
11461180
(): any;
11471181
}
11481182
export declare var Injectable: InjectableFactory;
1183+
export interface InjectFactory {
1184+
(binding?: any): any;
1185+
new (binding?: any): any;
1186+
}
1187+
export declare var Inject: InjectFactory;
11491188
export interface OnInit {
11501189
ngOnInit(): any;
11511190
}

0 commit comments

Comments
 (0)