From a1b6ad07a894421c4a741f6831d684d5e68f85e1 Mon Sep 17 00:00:00 2001 From: Mitchell Wills Date: Tue, 7 Jul 2020 01:27:23 +0000 Subject: [PATCH] fix(core): Allow passing AbstractType to the inject function (#37958) This is a type only change that replaces `Type|InjectionToken` with `Type|AbstractType|InjectionToken` in the injector. PR Close #37958 --- goldens/public-api/core/core.d.ts | 6 ++-- packages/core/src/di/inject_switch.ts | 16 +++++++---- packages/core/src/di/injector.ts | 7 +++-- .../core/src/di/injector_compatibility.ts | 20 ++++++------- packages/core/src/di/r3_injector.ts | 14 ++++++---- packages/core/src/render3/component_ref.ts | 6 ++-- packages/core/src/render3/di.ts | 19 +++++++------ packages/core/src/render3/instructions/di.ts | 9 +++--- .../core/src/render3/interfaces/injector.ts | 7 +++-- packages/core/test/di/r3_injector_spec.ts | 28 +++++++++++++++++++ packages/examples/core/di/ts/injector_spec.ts | 4 +-- 11 files changed, 88 insertions(+), 48 deletions(-) diff --git a/goldens/public-api/core/core.d.ts b/goldens/public-api/core/core.d.ts index 712fd46125..ad6dd6a01e 100644 --- a/goldens/public-api/core/core.d.ts +++ b/goldens/public-api/core/core.d.ts @@ -445,7 +445,7 @@ export declare class InjectionToken { } export declare abstract class Injector { - abstract get(token: Type | InjectionToken | AbstractType, notFoundValue?: T, flags?: InjectFlags): T; + abstract get(token: Type | AbstractType | InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; /** @deprecated */ abstract get(token: any, notFoundValue?: any): any; static NULL: Injector; static THROW_IF_NOT_FOUND: {}; @@ -676,8 +676,8 @@ export declare function ɵɵdefineInjectable(opts: { }): never; /** @codeGenApi */ -export declare function ɵɵinject(token: Type | InjectionToken): T; -export declare function ɵɵinject(token: Type | InjectionToken, flags?: InjectFlags): T | null; +export declare function ɵɵinject(token: Type | AbstractType | InjectionToken): T; +export declare function ɵɵinject(token: Type | AbstractType | InjectionToken, flags?: InjectFlags): T | null; /** @codeGenApi */ export declare interface ɵɵInjectableDef { diff --git a/packages/core/src/di/inject_switch.ts b/packages/core/src/di/inject_switch.ts index bc5527566e..9614aa2ec1 100644 --- a/packages/core/src/di/inject_switch.ts +++ b/packages/core/src/di/inject_switch.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '../interface/type'; +import {AbstractType, Type} from '../interface/type'; import {assertNotEqual} from '../util/assert'; import {stringify} from '../util/stringify'; import {InjectionToken} from './injection_token'; @@ -23,7 +23,8 @@ import {InjectFlags} from './interface/injector'; * 1. `Injector` should not depend on ivy logic. * 2. To maintain tree shake-ability we don't want to bring in unnecessary code. */ -let _injectImplementation: ((token: Type|InjectionToken, flags?: InjectFlags) => T | null)| +let _injectImplementation: + ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T | null)| undefined; export function getInjectImplementation() { return _injectImplementation; @@ -34,8 +35,10 @@ export function getInjectImplementation() { * Sets the current inject implementation. */ export function setInjectImplementation( - impl: ((token: Type|InjectionToken, flags?: InjectFlags) => T | null)| - undefined): ((token: Type|InjectionToken, flags?: InjectFlags) => T | null)|undefined { + impl: ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T | null)| + undefined): + ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T | null)| + undefined { const previous = _injectImplementation; _injectImplementation = impl; return previous; @@ -50,7 +53,8 @@ export function setInjectImplementation( * `InjectableDef`. */ export function injectRootLimpMode( - token: Type|InjectionToken, notFoundValue: T|undefined, flags: InjectFlags): T|null { + token: Type|AbstractType|InjectionToken, notFoundValue: T|undefined, + flags: InjectFlags): T|null { const injectableDef: ɵɵInjectableDef|null = getInjectableDef(token); if (injectableDef && injectableDef.providedIn == 'root') { return injectableDef.value === undefined ? injectableDef.value = injectableDef.factory() : @@ -70,7 +74,7 @@ export function injectRootLimpMode( * @param fn Function which it should not equal to */ export function assertInjectImplementationNotEqual( - fn: ((token: Type|InjectionToken, flags?: InjectFlags) => T | null)) { + fn: ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T | null)) { ngDevMode && assertNotEqual(_injectImplementation, fn, 'Calling ɵɵinject would cause infinite recursion'); } diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index 5fda8bfbf1..f4c5a37071 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -67,9 +67,9 @@ export abstract class Injector { * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. */ abstract get( - token: Type|InjectionToken|AbstractType, notFoundValue?: T, flags?: InjectFlags): T; + token: Type|AbstractType|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; /** - * @deprecated from v4.0.0 use Type or InjectionToken + * @deprecated from v4.0.0 use Type, AbstractType or InjectionToken * @suppress {duplicate} */ abstract get(token: any, notFoundValue?: any): any; @@ -156,7 +156,8 @@ export class StaticInjector implements Injector { this.scope = recursivelyProcessProviders(records, providers); } - get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; + get(token: Type|AbstractType|InjectionToken, notFoundValue?: T, flags?: InjectFlags): + T; get(token: any, notFoundValue?: any): any; get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { const records = this._records; diff --git a/packages/core/src/di/injector_compatibility.ts b/packages/core/src/di/injector_compatibility.ts index 7af8927653..71586aa127 100644 --- a/packages/core/src/di/injector_compatibility.ts +++ b/packages/core/src/di/injector_compatibility.ts @@ -8,7 +8,7 @@ import '../util/ng_dev_mode'; -import {Type} from '../interface/type'; +import {AbstractType, Type} from '../interface/type'; import {getClosureSafeProperty} from '../util/property'; import {stringify} from '../util/stringify'; import {resolveForwardRef} from './forward_ref'; @@ -46,12 +46,11 @@ export function setCurrentInjector(injector: Injector|null|undefined): Injector| return former; } - -export function injectInjectorOnly(token: Type|InjectionToken): T; -export function injectInjectorOnly(token: Type|InjectionToken, flags?: InjectFlags): T| - null; +export function injectInjectorOnly(token: Type|AbstractType|InjectionToken): T; export function injectInjectorOnly( - token: Type|InjectionToken, flags = InjectFlags.Default): T|null { + token: Type|AbstractType|InjectionToken, flags?: InjectFlags): T|null; +export function injectInjectorOnly( + token: Type|AbstractType|InjectionToken, flags = InjectFlags.Default): T|null { if (_currentInjector === undefined) { throw new Error(`inject() must be called from an injection context`); } else if (_currentInjector === null) { @@ -74,9 +73,11 @@ export function injectInjectorOnly( * @codeGenApi * @publicApi This instruction has been emitted by ViewEngine for some time and is deployed to npm. */ -export function ɵɵinject(token: Type|InjectionToken): T; -export function ɵɵinject(token: Type|InjectionToken, flags?: InjectFlags): T|null; -export function ɵɵinject(token: Type|InjectionToken, flags = InjectFlags.Default): T|null { +export function ɵɵinject(token: Type|AbstractType|InjectionToken): T; +export function ɵɵinject( + token: Type|AbstractType|InjectionToken, flags?: InjectFlags): T|null; +export function ɵɵinject( + token: Type|AbstractType|InjectionToken, flags = InjectFlags.Default): T|null { return (getInjectImplementation() || injectInjectorOnly)(resolveForwardRef(token), flags); } @@ -130,7 +131,6 @@ Please check that 1) the type for the parameter at index ${ */ export const inject = ɵɵinject; - export function injectArgs(types: (Type|InjectionToken|any[])[]): any[] { const args: any[] = []; for (let i = 0; i < types.length; i++) { diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index ab941aa62e..c993e6a738 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -9,7 +9,7 @@ import '../util/ng_dev_mode'; import {OnDestroy} from '../interface/lifecycle_hooks'; -import {Type} from '../interface/type'; +import {AbstractType, Type} from '../interface/type'; import {FactoryFn, getFactoryDef} from '../render3/definition_factory'; import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors_di'; import {deepForEach, newArray} from '../util/array_utils'; @@ -103,7 +103,7 @@ export class R3Injector { * - `null` value implies that we don't have the record. Used by tree-shakable injectors * to prevent further searches. */ - private records = new Map|InjectionToken, Record|null>(); + private records = new Map|AbstractType|InjectionToken, Record|null>(); /** * The transitive set of `InjectorType`s which define this injector. @@ -181,7 +181,7 @@ export class R3Injector { } get( - token: Type|InjectionToken, notFoundValue: any = THROW_IF_NOT_FOUND, + token: Type|AbstractType|InjectionToken, notFoundValue: any = THROW_IF_NOT_FOUND, flags = InjectFlags.Default): T { this.assertNotDestroyed(); // Set the injection context. @@ -404,7 +404,7 @@ export class R3Injector { this.records.set(token, record); } - private hydrate(token: Type|InjectionToken, record: Record): T { + private hydrate(token: Type|AbstractType|InjectionToken, record: Record): T { if (ngDevMode && record.value === CIRCULAR) { throwCyclicDependencyError(stringify(token)); } else if (record.value === NOT_YET) { @@ -428,7 +428,8 @@ export class R3Injector { } } -function injectableDefOrInjectorDefFactory(token: Type|InjectionToken): FactoryFn { +function injectableDefOrInjectorDefFactory(token: Type|AbstractType| + InjectionToken): FactoryFn { // Most tokens will have an injectable def directly on them, which specifies a factory directly. const injectableDef = getInjectableDef(token); const factory = injectableDef !== null ? injectableDef.factory : getFactoryDef(token); @@ -564,7 +565,8 @@ function hasOnDestroy(value: any): value is OnDestroy { typeof (value as OnDestroy).ngOnDestroy === 'function'; } -function couldBeInjectableType(value: any): value is Type|InjectionToken { +function couldBeInjectableType(value: any): value is Type|AbstractType| + InjectionToken { return (typeof value === 'function') || (typeof value === 'object' && value instanceof InjectionToken); } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index d74d69e053..a5d0679899 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -10,7 +10,7 @@ import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detec import {InjectionToken} from '../di/injection_token'; import {Injector} from '../di/injector'; import {InjectFlags} from '../di/interface/injector'; -import {Type} from '../interface/type'; +import {AbstractType, Type} from '../interface/type'; import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {createElementRef, ElementRef as viewEngine_ElementRef} from '../linker/element_ref'; @@ -80,7 +80,9 @@ export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDUL function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector { return { - get: (token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T => { + get: ( + token: Type|AbstractType|InjectionToken, notFoundValue?: T, + flags?: InjectFlags): T => { const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as T, flags); if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR || diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 01f5897431..0decb16f94 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -13,7 +13,7 @@ import {Injector} from '../di/injector'; import {InjectorMarkers} from '../di/injector_marker'; import {getInjectorDef} from '../di/interface/defs'; import {InjectFlags} from '../di/interface/injector'; -import {Type} from '../interface/type'; +import {AbstractType, Type} from '../interface/type'; import {assertDefined, assertEqual, assertIndexInRange} from '../util/assert'; import {noSideEffects} from '../util/closure'; @@ -347,7 +347,8 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str function notFoundValueOrThrow( - notFoundValue: T|null, token: Type|InjectionToken, flags: InjectFlags): T|null { + notFoundValue: T|null, token: Type|AbstractType|InjectionToken, flags: InjectFlags): T| + null { if (flags & InjectFlags.Optional) { return notFoundValue; } else { @@ -365,8 +366,8 @@ function notFoundValueOrThrow( * @returns the value from the injector or throws an exception */ function lookupTokenUsingModuleInjector( - lView: LView, token: Type|InjectionToken, flags: InjectFlags, notFoundValue?: any): T| - null { + lView: LView, token: Type|AbstractType|InjectionToken, flags: InjectFlags, + notFoundValue?: any): T|null { if (flags & InjectFlags.Optional && notFoundValue === undefined) { // This must be set or the NullInjector will throw for optional deps notFoundValue = null; @@ -408,7 +409,7 @@ function lookupTokenUsingModuleInjector( * @returns the value from the injector, `null` when not found, or `notFoundValue` if provided */ export function getOrCreateInjectable( - tNode: TDirectiveHostNode|null, lView: LView, token: Type|InjectionToken, + tNode: TDirectiveHostNode|null, lView: LView, token: Type|AbstractType|InjectionToken, flags: InjectFlags = InjectFlags.Default, notFoundValue?: any): T|null { if (tNode !== null) { const bloomHash = bloomHashBitOrFactory(token); @@ -508,7 +509,7 @@ export function createNodeInjector(): Injector { } function searchTokensOnInjector( - injectorIndex: number, lView: LView, token: Type|InjectionToken, + injectorIndex: number, lView: LView, token: Type|AbstractType|InjectionToken, previousTView: TView|null, flags: InjectFlags, hostTElementNode: TNode|null) { const currentTView = lView[TVIEW]; const tNode = currentTView.data[injectorIndex + NodeInjectorOffset.TNODE] as TNode; @@ -555,7 +556,7 @@ function searchTokensOnInjector( * @returns Index of a found directive or provider, or null when none found. */ export function locateDirectiveOrProvider( - tNode: TNode, tView: TView, token: Type|InjectionToken|string, + tNode: TNode, tView: TView, token: Type|AbstractType|InjectionToken|string, canAccessViewProviders: boolean, isHostSpecialCase: boolean|number): number|null { const nodeProviderIndexes = tNode.providerIndexes; const tInjectables = tView.data; @@ -646,8 +647,8 @@ export function getNodeInjectable( * @returns the matching bit to check in the bloom filter or `null` if the token is not known. * When the returned value is negative then it represents special values such as `Injector`. */ -export function bloomHashBitOrFactory(token: Type|InjectionToken|string): number|Function| - undefined { +export function bloomHashBitOrFactory(token: Type|AbstractType|InjectionToken| + string): number|Function|undefined { ngDevMode && assertDefined(token, 'token must be defined'); if (typeof token === 'string') { return token.charCodeAt(0) || 0; diff --git a/packages/core/src/render3/instructions/di.ts b/packages/core/src/render3/instructions/di.ts index 3e84551a28..52a4dc83f8 100644 --- a/packages/core/src/render3/instructions/di.ts +++ b/packages/core/src/render3/instructions/di.ts @@ -8,7 +8,7 @@ import {InjectFlags, InjectionToken, resolveForwardRef} from '../../di'; import {assertInjectImplementationNotEqual} from '../../di/inject_switch'; import {ɵɵinject} from '../../di/injector_compatibility'; -import {Type} from '../../interface/type'; +import {AbstractType, Type} from '../../interface/type'; import {getOrCreateInjectable} from '../di'; import {TDirectiveHostNode} from '../interfaces/node'; import {getCurrentTNode, getLView} from '../state'; @@ -37,10 +37,11 @@ import {getCurrentTNode, getLView} from '../state'; * * @codeGenApi */ -export function ɵɵdirectiveInject(token: Type|InjectionToken): T; -export function ɵɵdirectiveInject(token: Type|InjectionToken, flags: InjectFlags): T; +export function ɵɵdirectiveInject(token: Type|AbstractType|InjectionToken): T; export function ɵɵdirectiveInject( - token: Type|InjectionToken, flags = InjectFlags.Default): T|null { + token: Type|AbstractType|InjectionToken, flags: InjectFlags): T; +export function ɵɵdirectiveInject( + token: Type|AbstractType|InjectionToken, flags = InjectFlags.Default): T|null { const lView = getLView(); // Fall back to inject() if view hasn't been created. This situation can happen in tests // if inject utilities are used before bootstrapping. diff --git a/packages/core/src/render3/interfaces/injector.ts b/packages/core/src/render3/interfaces/injector.ts index 6762562651..5a06dc39fb 100644 --- a/packages/core/src/render3/interfaces/injector.ts +++ b/packages/core/src/render3/interfaces/injector.ts @@ -8,7 +8,7 @@ import {InjectionToken} from '../../di/injection_token'; import {InjectFlags} from '../../di/interface/injector'; -import {Type} from '../../interface/type'; +import {AbstractType, Type} from '../../interface/type'; import {assertDefined, assertEqual} from '../../util/assert'; import {TDirectiveHostNode} from './node'; @@ -176,7 +176,8 @@ export class NodeInjectorFactory { /** * The inject implementation to be activated when using the factory. */ - injectImpl: null|((token: Type|InjectionToken, flags?: InjectFlags) => T); + injectImpl: null| + ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T); /** * Marker set to true during factory invocation to see if we get into recursive loop. @@ -280,7 +281,7 @@ export class NodeInjectorFactory { */ isViewProvider: boolean, injectImplementation: null| - ((token: Type|InjectionToken, flags?: InjectFlags) => T)) { + ((token: Type|AbstractType|InjectionToken, flags?: InjectFlags) => T)) { ngDevMode && assertDefined(factory, 'Factory not specified'); ngDevMode && assertEqual(typeof factory, 'function', 'Expected factory function.'); this.canSeeViewProviders = isViewProvider; diff --git a/packages/core/test/di/r3_injector_spec.ts b/packages/core/test/di/r3_injector_spec.ts index 4da5df4753..94b804492e 100644 --- a/packages/core/test/di/r3_injector_spec.ts +++ b/packages/core/test/di/r3_injector_spec.ts @@ -180,6 +180,15 @@ describe('InjectorDef-based createInjector()', () => { class ChildService extends ServiceWithDep {} + abstract class AbstractService { + static ɵprov = ɵɵdefineInjectable({ + token: AbstractService, + providedIn: null, + factory: () => new AbstractServiceImpl(), + }); + } + class AbstractServiceImpl extends AbstractService {} + class Module { static ɵinj = ɵɵdefineInjector({ factory: () => new Module(), @@ -200,10 +209,17 @@ describe('InjectorDef-based createInjector()', () => { CircularB, {provide: STATIC_TOKEN, useClass: StaticService, deps: [Service]}, InjectorWithDep, + AbstractService, ], }); } + const ABSTRACT_SERVICE_TOKEN_WITH_FACTORY = + new InjectionToken('ABSTRACT_SERVICE_TOKEN', { + providedIn: Module, + factory: () => ɵɵinject(AbstractService), + }); + class OtherModule { static ɵinj = ɵɵdefineInjector({ factory: () => new OtherModule(), @@ -457,6 +473,18 @@ describe('InjectorDef-based createInjector()', () => { expect(injector.get(ImportsNotAModule)).toBeDefined(); }); + it('injects an abstract class', () => { + const instance = injector.get(AbstractService); + expect(instance instanceof AbstractServiceImpl).toBeTruthy(); + expect(injector.get(AbstractService)).toBe(instance); + }); + + it('injects an abstract class in an InjectionToken factory', () => { + const instance = injector.get(ABSTRACT_SERVICE_TOKEN_WITH_FACTORY); + expect(instance instanceof AbstractServiceImpl).toBeTruthy(); + expect(injector.get(ABSTRACT_SERVICE_TOKEN_WITH_FACTORY)).toBe(instance); + }); + describe('error handling', () => { it('throws an error when a token is not found', () => { expect(() => injector.get(ServiceTwo)).toThrow(); diff --git a/packages/examples/core/di/ts/injector_spec.ts b/packages/examples/core/di/ts/injector_spec.ts index fc599494d3..dcb0037298 100644 --- a/packages/examples/core/di/ts/injector_spec.ts +++ b/packages/examples/core/di/ts/injector_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {inject, InjectFlags, InjectionToken, Injector, Type, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; +import {AbstractType, inject, InjectFlags, InjectionToken, Injector, Type, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; class MockRootScopeInjector implements Injector { constructor(readonly parent: Injector) {} get( - token: Type|InjectionToken, defaultValue?: any, + token: Type|AbstractType|InjectionToken, defaultValue?: any, flags: InjectFlags = InjectFlags.Default): T { if ((token as any).ɵprov && (token as any).ɵprov.providedIn === 'root') { const old = setCurrentInjector(this);