8000 fix(core): missing useExisting providers throwing for optional calls … · angular/angular@4623b61 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4623b61

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): missing useExisting providers throwing for optional calls (#61152)
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 #61121. PR Close #61152
1 parent e9e1c43 commit 4623b61

File tree

5 files changed

+114
-12
lines changed

5 files changed

+114
-12
lines changed

packages/core/src/di/r3_injector.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function getNullInjector(): Injector {
111111
* current value.
112112
*/
113113
interface Record<T> {
114-
factory: (() => T) | undefined;
114+
factory: ((_: undefined, flags?: InjectFlags) => T) | undefined;
115115
value: T | {};
116116
multi: any[] | undefined;
117117
}
@@ -349,7 +349,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
349349
}
350350
// If a record was found, get the instance for it and return it.
351351
if (record != null /* NOT null || undefined */) {
352-
return this.hydrate(token, record);
352+
return this.hydrate(token, record, flags);
353353
}
354354
}
355355

@@ -477,7 +477,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
477477
this.records.set(token, record);
478478
}
479479

480-
private hydrate<T>(token: ProviderToken<T>, record: Record<T>): T {
480+
private hydrate<T>(token: ProviderToken<T>, record: Record<T>, flags: InjectFlags): T {
481481
const prevConsumer = setActiveConsumer(null);
482482
try {
483483
if (record.value === CIRCULAR) {
@@ -487,11 +487,11 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
487487

488488
if (ngDevMode) {
489489
runInInjectorProfilerContext(this, token as Type<T>, () => {
490-
record.value = record.factory!();
490+
record.value = record.factory!(undefined, flags);
491491
emitInstanceCreatedByInjectorEvent(record.value);
492492
});
493493
} else {
494-
record.value = record.factory!();
494+
record.value = record.factory!(undefined, flags);
495495
}
496496
}
497497
if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) {
@@ -580,7 +580,8 @@ function providerToRecord(provider: SingleProvider): Record<any> {
580580
if (isValueProvider(provider)) {
581581
return makeRecord(undefined, provider.useValue);
582582
} else {
583-
const factory: (() => any) | undefined = providerToFactory(provider);
583+
const factory: ((type?: Type<unknown>, flags?: InjectFlags) => any) | undefined =
584+
providerToFactory(provider);
584585
return makeRecord(factory, NOT_YET);
585586
}
586587
}
@@ -594,8 +595,8 @@ export function providerToFactory(
594595
provider: SingleProvider,
595596
ngModuleType?: InjectorType<any>,
596597
providers?: any[],
597-
): () => any {
598-
let factory: (() => any) | undefined = undefined;
598+
): (type?: Type<unknown>, flags?: number) => any {
599+
let factory: ((type?: Type<unknown>, flags?: InjectFlags) => any) | undefined = undefined;
599600
if (ngDevMode && isEnvironmentProviders(provider)) {
600601
throwInvalidProviderError(undefined, providers, provider);
601602
}
@@ -609,7 +610,11 @@ export function providerToFactory(
609610
} else if (isFactoryProvider(provider)) {
610611
factory = () => provider.useFactory(...injectArgs(provider.deps || []));
611612
} else if (isExistingProvider(provider)) {
612-
factory = () => ɵɵinject(resolveForwardRef(provider.useExisting));
613+
factory = (_, flags) =>
614+
ɵɵinject(
615+
resolveForwardRef(provider.useExisting),
616+
flags !== undefined && flags & InjectFlags.Optional ? InjectFlags.Optional : undefined,
617+
);
613618
} else {
614619
const classRef = resolveForwardRef(
615620
provider &&

packages/core/src/render3/di.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ function searchTokensOnInjector<T>(
659659
isHostSpecialCase,
660660
);
661661
if (injectableIdx !== null) {
662-
return getNodeInjectable(lView, currentTView, injectableIdx, tNode as TElementNode);
662+
return getNodeInjectable(lView, currentTView, injectableIdx, tNode as TElementNode, flags);
663663
} else {
664664
return NOT_FOUND;
665665
}
@@ -725,6 +725,7 @@ export function getNodeInjectable(
725725
tView: TView,
726726
index: number,
727727
tNode: TDirectiveHostNode,
728+
flags?: InjectFlags,
728729
): any {
729730
let value = lView[index];
730731
const tData = tView.data;
@@ -759,7 +760,7 @@ export function getNodeInjectable(
759760
"Because flags do not contain `SkipSelf' we expect this to always succeed.",
760761
);
761762
try {
762-
value = lView[index] = factory.factory(undefined, tData, lView, tNode);
763+
value = lView[index] = factory.factory(undefined, flags, tData, lView, tNode);
763764

764765
ngDevMode && emitInstanceCreatedByInjectorEvent(value);
765766

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 {InjectFlags, 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: InjectFlags | 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: InjectFlags | 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: InjectFlags | 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: InjectFlags | 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
@@ -4469,6 +4469,94 @@ describe('di', () => {
44694469
});
44704470
});
44714471
< EED3 br>
4472+
describe('useExisting and optional', () => {
4473+
const token = new InjectionToken('token');
4474+
const existing = new InjectionToken('existing');
4475+
4476+
it('should return null when injecting a missing useExisting provider with optional: true in a node injector', () => {
4477+
let value: unknown;
4478+
4479+
@Directive({selector: '[dir]'})
4480+
class Dir {
4481+
constructor() {
4482+
value = inject(token, {optional: true});
4483+
}
4484+
}
4485+
4486+
@Component({
4487+
template: '<div dir></div>',
4488+
imports: [Dir],
4489+
providers: [{provide: token, useExisting: existing}],
4490+
})
4491+
class App {}
4492+
4493+
TestBed.createComponent(App);
4494+
expect(value).toBe(null);
4495+
});
4496+
4497+
it('should throw when injecting a missing useExisting provider in a node injector', () => {
4498+
@Directive({selector: '[dir]'})
4499+
class Dir {
4500+
constructor() {
4501+
inject(token, {optional: false});
4502+
}
4503+
}
4504+
4505+
@Component({
4506+
template: '<div dir></div>',
4507+
imports: [Dir],
4508+
providers: [{provide: token, useExisting: existing}],
4509+
})
4510+
class App {}
4511+
4512+
expect(() => TestBed.createComponent(App)).toThrowError(
4513+
/No provider for InjectionToken existing/,
4514+
);
4515+
});
4516+
4517+
it('should return null when injecting a missing useExisting provider with optional: true in a module injector', () => {
4518+
let value: unknown;
4519+
4520+
@Directive({selector: '[dir]', standalone: false})
4521+
class Dir {
4522+
constructor() {
4523+
value = inject(token, {optional: true});
4524+
}
4525+
}
4526+
4527+
@Component({template: '<div dir></div>', standalone: false})
4528+
class App {}
4529+
4530+
TestBed.configureTestingModule({
4531+
declarations: [App, Dir],
4532+
providers: [{provide: token, useExisting: existing}],
4533+
});
4534+
TestBed.createComponent(App);
4535+
expect(value).toBe(null);
4536+
});
4537+
4538+
it('should throw when injecting a missing useExisting provider in a module injector', () => {
4539+
@Directive({selector: '[dir]', standalone: false})
4540+
class Dir {
4541+
constructor() {
4542+
inject(token);
4543+
}
4544+
}
4545+
4546+
@Component({template: '<div dir></div>', standalone: false})
4547+
class App {}
4548+
4549+
TestBed.configureTestingModule({
4550+
declarations: [App, Dir],
4551+
providers: [{provide: token, useExisting: existing}],
4552+
});
4553+
4554+
expect(() => TestBed.createComponent(App)).toThrowError(
4555+
/No provider 6247 for InjectionToken existing/,
4556+
);
4557+
});
4558+
});
4559+
44724560
it('should be able to use Host in `useFactory` dependency config', () => {
44734561
// Scenario:
44744562
// ---------

0 commit comments

Comments
 (0)
0