Skip to content

Commit 583b9a7

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): missing useExisting providers throwing for optional calls (angular#61137)
Fixes that the runtime was throwing a DI error when attempting to inject a missing `useExisting` provider, despite the call being optional. The problem was that when the provider has `useExisting`, we do a second `inject` call under the hood which didn't include the inject flags from the original call. Fixes angular#61121. PR Close angular#61137
1 parent 1405387 commit 583b9a7

File tree

5 files changed

+116
-12
lines changed

5 files changed

+116
-12
lines changed

packages/core/src/di/r3_injector.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function getNullInjector(): Injector {
114114
* current value.
115115
*/
116116
interface Record<T> {
117-
factory: (() => T) | undefined;
117+
factory: ((_: undefined, flags?: InternalInjectFlags) => T) | undefined;
118118
value: T | {};
119119
multi: any[] | undefined;
120120
}
@@ -360,7 +360,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
360360
}
361361
// If a record was found, get the instance for it and return it.
362362
if (record != null /* NOT null || undefined */) {
363-
return this.hydrate(token, record);
363+
return this.hydrate(token, record, flags);
364364
}
365365
}
366366

@@ -492,7 +492,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
492492
this.records.set(token, record);
493493
}
494494

495-
private hydrate<T>(token: ProviderToken<T>, record: Record<T>): T {
495+
private hydrate<T>(token: ProviderToken<T>, record: Record<T>, flags: InternalInjectFlags): T {
496496
const prevConsumer = setActiveConsumer(null);
497497
try {
498498
if (record.value === CIRCULAR) {
@@ -503,11 +503,11 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
503503
if (ngDevMode) {
504504
runInInjectorProfilerContext(this, token as Type<T>, () => {
505505
emitInjectorToCreateInstanceEvent(token);
506-
record.value = record.factory!();
506+
record.value = record.factory!(undefined, flags);
507507
emitInstanceCreatedByInjectorEvent(record.value);
508508
});
509509
} else {
510-
record.value = record.factory!();
510+
record.value = record.factory!(undefined, flags);
511511
}
512512
}
513513
if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) {
@@ -596,7 +596,8 @@ function providerToRecord(provider: SingleProvider): Record<any> {
596596
if (isValueProvider(provider)) {
597597
return makeRecord(undefined, provider.useValue);
598598
} else {
599-
const factory: (() => any) | undefined = providerToFactory(provider);
599+
const factory: ((type?: Type<unknown>, flags?: InternalInjectFlags) => any) | undefined =
600+
providerToFactory(provider);
600601
return makeRecord(factory, NOT_YET);
601602
}
602603
}
@@ -610,8 +611,8 @@ export function providerToFactory(
610611
provider: SingleProvider,
611612
ngModuleType?: InjectorType<any>,
612613
providers?: any[],
613-
): () => any {
614-
let factory: (() => any) | undefined = undefined;
614+
): (type?: Type<unknown>, flags?: number) => any {
615+
let factory: ((type?: Type<unknown>, flags?: InternalInjectFlags) => any) | undefined = undefined;
615616
if (ngDevMode && isEnvironmentProviders(provider)) {
616617
throwInvalidProviderError(undefined, providers, provider);
617618
}
@@ -625,7 +626,13 @@ export function providerToFactory(
625626
} else if (isFactoryProvider(provider)) {
626627
factory = () => provider.useFactory(...injectArgs(provider.deps || []));
627628
} else if (isExistingProvider(provider)) {
628-
factory = () => ɵɵinject(resolveForwardRef(provider.useExisting));
629+
factory = (_, flags) =>
630+
ɵɵinject(
631+
resolveForwardRef(provider.useExisting),
632+
flags !== undefined && flags & InternalInjectFlags.Optional
633+
? InternalInjectFlags.Optional
634+
: undefined,
635+
);
629636
} else {
630637
const classRef = resolveForwardRef(
631638
provider &&

packages/core/src/render3/di.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ function searchTokensOnInjector<T>(
662662
isHostSpecialCase,
663663
);
664664
if (injectableIdx !== null) {
665-
return getNodeInjectable(lView, currentTView, injectableIdx, tNode as TElementNode);
665+
return getNodeInjectable(lView, currentTView, injectableIdx, tNode as TElementNode, flags);
666666
} else {
667667
return NOT_FOUND;
668668
}
@@ -728,6 +728,7 @@ export function getNodeInjectable(
728728
tView: TView,
729729
index: number,
730730
tNode: TDirectiveHostNode,
731+
flags?: InternalInjectFlags,
731732
): any {
732733
let value = lView[index];
733734
const tData = tView.data;
@@ -765,7 +766,7 @@ export function getNodeInjectable(
765766
try {
766767
ngDevMode && emitInjectorToCreateInstanceEvent(token);
767768

768-
value = lView[index] = factory.factory(undefined, tData, lView, tNode);
769+
value = lView[index] = factory.factory(undefined, flags, tData, lView, tNode);
769770

770771
ngDevMode && emitInstanceCreatedByInjectorEvent(value);
771772

packages/core/src/render3/di_setup.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {resolveForwardRef} from '../di/forward_ref';
10+
import {InternalInjectFlags} from '../di/interface/injector';
1011
import {ClassProvider, Provider} from '../di/interface/provider';
1112
import {isClassProvider, isTypeProvider, SingleProvider} from '../di/provider_collection';
1213
import {providerToFactory} from '../di/r3_injector';
@@ -319,6 +320,7 @@ function indexOf(item: any, arr: any[], begin: number, end: number) {
319320
function multiProvidersFactoryResolver(
320321
this: NodeInjectorFactory,
321322
_: undefined,
323+
flags: InternalInjectFlags | undefined,
322324
tData: TData,
323325
lData: LView,
324326
tNode: TDirectiveHostNode,
@@ -334,7 +336,8 @@ function multiProvidersFactoryResolver(
334336
function multiViewProvidersFactoryResolver(
335337
this: NodeInjectorFactory,
336338
_: undefined,
337-
tData: TData,
339+
_flags: InternalInjectFlags | undefined,
340+
_tData: TData,
338341
lView: LView,
339342
tNode: TDirectiveHostNode,
340343
): any[] {
@@ -382,6 +385,7 @@ function multiFactory(
382385
factoryFn: (
383386
this: NodeInjectorFactory,
384387
_: undefined,
388+
flags: InternalInjectFlags | undefined,
385389
tData: TData,
386390
lData: LView,
387391
tNode: TDirectiveHostNode,

packages/core/src/render3/interfaces/injector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ export class NodeInjectorFactory {
260260
public factory: (
261261
this: NodeInjectorFactory,
262262
_: undefined,
263+
/**
264+
* Flags that control the injection behavior.
265+
*/
266+
flags: InternalInjectFlags | undefined,
263267
/**
264268
* array where injectables tokens are stored. This is used in
265269
* case of an error reporting to produce friendlier errors.

packages/core/test/acceptance/di_spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4439,6 +4439,94 @@ describe('di', () => {
44394439
});
44404440
});
44414441

4442+
describe('useExisting and optional', () => {
4443+
const token = new InjectionToken('token');
4444+
const existing = new InjectionToken('existing');
4445+
4446+
it('should return null when injecting a missing useExisting provider with optional: true in a node injector', () => {
4447+
let value: unknown;
4448+
4449+
@Directive({selector: '[dir]'})
4450+
class Dir {
4451+
constructor() {
4452+
value = inject(token, {optional: true});
4453+
}
4454+
}
4455+
4456+
@Component({
4457+
template: '<div dir></div>',
4458+
imports: [Dir],
4459+
providers: [{provide: token, useExisting: existing}],
4460+
})
4461+
class App {}
4462+
4463+
TestBed.createComponent(App);
4464+
expect(value).toBe(null);
4465+
});
4466+
4467+
it('should throw when injecting a missing useExisting provider in a node injector', () => {
4468+
@Directive({selector: '[dir]'})
4469+
class Dir {
4470+
constructor() {
4471+
inject(token, {optional: false});
4472+
}
4473+
}
4474+
4475+
@Component({
4476+
template: '<div dir></div>',
4477+
imports: [Dir],
4478+
providers: [{provide: token, useExisting: existing}],
4479+
})
4480+
class App {}
4481+
4482+
expect(() => TestBed.createComponent(App)).toThrowError(
4483+
/No provider for InjectionToken existing/,
4484+
);
4485+
});
4486+
4487+
it('should return null when injecting a missing useExisting provider with optional: true in a module injector', () => {
4488+
let value: unknown;
4489+
4490+
@Directive({selector: '[dir]', standalone: false})
4491+
class Dir {
4492+
constructor() {
4493+
value = inject(token, {optional: true});
4494+
}
4495+
}
4496+
4497+
@Component({template: '<div dir></div>', standalone: false})
4498+
class App {}
4499+
4500+
TestBed.configureTestingModule({
4501+
declarations: [App, Dir],
4502+
providers: [{provide: token, useExisting: existing}],
4503+
});
4504+
TestBed.createComponent(App);
4505+
expect(value).toBe(null);
4506+
});
4507+
4508+
it('should throw when injecting a missing useExisting provider in a module injector', () => {
4509+
@Directive({selector: '[dir]', standalone: false})
4510+
class Dir {
4511+
constructor() {
4512+
inject(token);
4513+
}
4514+
}
4515+
4516+
@Component({template: '<div dir></div>', standalone: false})
4517+
class App {}
4518+
4519+
TestBed.configureTestingModule({
4520+
declarations: [App, Dir],
4521+
providers: [{provide: token, useExisting: existing}],
4522+
});
4523+
4524+
expect(() => TestBed.createComponent(App)).toThrowError(
4525+
/No provider for InjectionToken existing/,
4526+
);
4527+
});
4528+
});
4529+
44424530
it('should be able to use Host in `useFactory` dependency config', () => {
44434531
// Scenario:
44444532
// ---------

0 commit comments

Comments
 (0)