fix(ivy): ngtsc should include generic types on injectable definitions (#27037)

Analogously to directives, the `ngInjectableDef` field in .d.ts files is
annotated with the type of service that it represents. If the service
contains required generic type arguments, these must be included in
the .d.ts file.

PR Close #27037
This commit is contained in:
JoostK 2018-11-10 02:58:33 +01:00 committed by Igor Minar
parent 7f221d8d2a
commit 159ab1c257
7 changed files with 34 additions and 12 deletions

View File

@ -74,6 +74,7 @@ function extractInjectableMetadata(
const name = clazz.name.text;
const type = new WrappedNodeExpr(clazz.name);
const ctorDeps = getConstructorDependencies(clazz, reflector, isCore);
const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0;
if (decorator.args === null) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
@ -82,6 +83,7 @@ function extractInjectableMetadata(
return {
name,
type,
typeArgumentCount,
providedIn: new LiteralExpr(null), ctorDeps,
};
} else if (decorator.args.length === 1) {
@ -118,6 +120,7 @@ function extractInjectableMetadata(
return {
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useValue: new WrappedNodeExpr(meta.get('useValue') !)
@ -126,6 +129,7 @@ function extractInjectableMetadata(
return {
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useExisting: new WrappedNodeExpr(meta.get('useExisting') !)
@ -134,6 +138,7 @@ function extractInjectableMetadata(
return {
name,
type,
typeArgumentCount,
ctorDeps,
providedIn,
useClass: new WrappedNodeExpr(meta.get('useClass') !), userDeps
@ -141,9 +146,9 @@ function extractInjectableMetadata(
} else if (meta.has('useFactory')) {
// useFactory is special - the 'deps' property must be analyzed.
const factory = new WrappedNodeExpr(meta.get('useFactory') !);
return {name, type, providedIn, useFactory: factory, ctorDeps, userDeps};
return {name, type, typeArgumentCount, providedIn, useFactory: factory, ctorDeps, userDeps};
} else {
return {name, type, providedIn, ctorDeps};
return {name, type, typeArgumentCount, providedIn, ctorDeps};
}
} else {
throw new FatalDiagnosticError(

View File

@ -46,6 +46,24 @@ describe('ngtsc behavioral tests', () => {
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵInjectableDef<Service>;');
});
it('should compile Injectables with a generic service', () => {
env.tsconfig();
env.write('test.ts', `
import {Injectable} from '@angular/core';
@Injectable()
export class Store<T> {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('Store.ngInjectableDef =');
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents).toContain('static ngInjectableDef: i0.ɵInjectableDef<Store<any>>;');
});
it('should compile Components without errors', () => {
env.tsconfig();
env.write('test.ts', `

View File

@ -78,6 +78,7 @@ export interface R3PipeMetadataFacade {
export interface R3InjectableMetadataFacade {
name: string;
type: any;
typeArgumentCount: number;
ctorDeps: R3DependencyMetadataFacade[]|null;
providedIn: any;
useClass?: any;

View File

@ -10,7 +10,7 @@ import {InjectFlags} from './core';
import {Identifiers} from './identifiers';
import * as o from './output/output_ast';
import {R3DependencyMetadata, R3FactoryDelegateType, R3FactoryMetadata, compileFactoryFunction} from './render3/r3_factory';
import {mapToMapExpression} from './render3/util';
import {mapToMapExpression, typeWithParameters} from './render3/util';
export interface InjectableDef {
expression: o.Expression;
@ -21,6 +21,7 @@ export interface InjectableDef {
export interface R3InjectableMetadata {
name: string;
type: o.Expression;
typeArgumentCount: number;
ctorDeps: R3DependencyMetadata[]|null;
providedIn: o.Expression;
useClass?: o.Expression;
@ -33,10 +34,6 @@ export interface R3InjectableMetadata {
export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
let result: {factory: o.Expression, statements: o.Statement[]}|null = null;
function makeFn(ret: o.Expression): o.Expression {
return o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`);
}
const factoryMeta = {
name: meta.name,
type: meta.type,
@ -100,8 +97,8 @@ export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
const expression = o.importExpr(Identifiers.defineInjectable).callFn([mapToMapExpression(
{token, factory: result.factory, providedIn})]);
const type = new o.ExpressionType(
o.importExpr(Identifiers.InjectableDef, [new o.ExpressionType(meta.type)]));
const type = new o.ExpressionType(o.importExpr(
Identifiers.InjectableDef, [typeWithParameters(meta.type, meta.typeArgumentCount)]));
return {
expression,

View File

@ -45,6 +45,7 @@ export class CompilerFacadeImpl implements CompilerFacade {
const {expression, statements} = compileInjectable({
name: facade.name,
type: new WrappedNodeExpr(facade.type),
typeArgumentCount: facade.typeArgumentCount,
providedIn: computeProvidedIn(facade.providedIn),
useClass: wrapExpression(facade, USE_CLASS),
useFactory: wrapExpression(facade, USE_FACTORY),

View File

@ -78,6 +78,7 @@ export interface R3PipeMetadataFacade {
export interface R3InjectableMetadataFacade {
name: string;
type: any;
typeArgumentCount: number;
ctorDeps: R3DependencyMetadataFacade[]|null;
providedIn: any;
useClass?: any;

View File

@ -23,9 +23,6 @@ import {convertDependencies, reflectDependencies} from './util';
* `ngInjectableDef` onto the injectable type.
*/
export function compileInjectable(type: Type<any>, srcMeta?: Injectable): void {
// Allow the compilation of a class with a `@Injectable()` decorator without parameters
const meta: Injectable = srcMeta || {providedIn: null};
let def: any = null;
// if NG_INJECTABLE_DEF is already defined on this class then don't overwrite it
@ -34,6 +31,7 @@ export function compileInjectable(type: Type<any>, srcMeta?: Injectable): void {
Object.defineProperty(type, NG_INJECTABLE_DEF, {
get: () => {
if (def === null) {
// Allow the compilation of a class with a `@Injectable()` decorator without parameters
const meta: Injectable = srcMeta || {providedIn: null};
const hasAProvider = isUseClassProvider(meta) || isUseFactoryProvider(meta) ||
isUseValueProvider(meta) || isUseExistingProvider(meta);
@ -42,6 +40,7 @@ export function compileInjectable(type: Type<any>, srcMeta?: Injectable): void {
const compilerMeta: R3InjectableMetadataFacade = {
name: type.name,
type: type,
typeArgumentCount: 0,
providedIn: meta.providedIn,
ctorDeps: reflectDependencies(type),
userDeps: undefined