fix(ivy): injecting incorrect provider when re-providing injectable with useClass (#34574)
If an injectable has a `useClass`, Ivy injects the token in `useClass`, rather than the original injectable, if the injectable is re-provided under a different token. The correct behavior is that it should inject the re-provided token, no matter whether it has `useClass`. Fixes #34110. PR Close #34574
This commit is contained in:
parent
ec789b0198
commit
0bc35a71e2
|
@ -12,6 +12,7 @@ import {OnDestroy} from '../interface/lifecycle_hooks';
|
||||||
import {Type} from '../interface/type';
|
import {Type} from '../interface/type';
|
||||||
import {getFactoryDef} from '../render3/definition';
|
import {getFactoryDef} from '../render3/definition';
|
||||||
import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors';
|
import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors';
|
||||||
|
import {FactoryFn} from '../render3/interfaces/definition';
|
||||||
import {deepForEach, newArray} from '../util/array_utils';
|
import {deepForEach, newArray} from '../util/array_utils';
|
||||||
import {stringify} from '../util/stringify';
|
import {stringify} from '../util/stringify';
|
||||||
import {resolveForwardRef} from './forward_ref';
|
import {resolveForwardRef} from './forward_ref';
|
||||||
|
@ -407,7 +408,7 @@ export class R3Injector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectableDefOrInjectorDefFactory(token: Type<any>| InjectionToken<any>): () => any {
|
function injectableDefOrInjectorDefFactory(token: Type<any>| InjectionToken<any>): FactoryFn<any> {
|
||||||
// Most tokens will have an injectable def directly on them, which specifies a factory directly.
|
// Most tokens will have an injectable def directly on them, which specifies a factory directly.
|
||||||
const injectableDef = getInjectableDef(token);
|
const injectableDef = getInjectableDef(token);
|
||||||
const factory = injectableDef !== null ? injectableDef.factory : getFactoryDef(token);
|
const factory = injectableDef !== null ? injectableDef.factory : getFactoryDef(token);
|
||||||
|
@ -478,7 +479,8 @@ export function providerToFactory(
|
||||||
provider: SingleProvider, ngModuleType?: InjectorType<any>, providers?: any[]): () => any {
|
provider: SingleProvider, ngModuleType?: InjectorType<any>, providers?: any[]): () => any {
|
||||||
let factory: (() => any)|undefined = undefined;
|
let factory: (() => any)|undefined = undefined;
|
||||||
if (isTypeProvider(provider)) {
|
if (isTypeProvider(provider)) {
|
||||||
return injectableDefOrInjectorDefFactory(resolveForwardRef(provider));
|
const unwrappedProvider = resolveForwardRef(provider);
|
||||||
|
return getFactoryDef(unwrappedProvider) || injectableDefOrInjectorDefFactory(unwrappedProvider);
|
||||||
} else {
|
} else {
|
||||||
if (isValueProvider(provider)) {
|
if (isValueProvider(provider)) {
|
||||||
factory = () => resolveForwardRef(provider.useValue);
|
factory = () => resolveForwardRef(provider.useValue);
|
||||||
|
@ -496,7 +498,7 @@ export function providerToFactory(
|
||||||
if (hasDeps(provider)) {
|
if (hasDeps(provider)) {
|
||||||
factory = () => new (classRef)(...injectArgs(provider.deps));
|
factory = () => new (classRef)(...injectArgs(provider.deps));
|
||||||
} else {
|
} else {
|
||||||
return injectableDefOrInjectorDefFactory(classRef);
|
return getFactoryDef(classRef) || injectableDefOrInjectorDefFactory(classRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1066,6 +1066,112 @@ describe('di', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('service injection with useClass', () => {
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
class BarServiceDep {
|
||||||
|
name = 'BarServiceDep';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
class BarService {
|
||||||
|
constructor(public dep: BarServiceDep) {}
|
||||||
|
getMessage() { return 'bar'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
class FooServiceDep {
|
||||||
|
name = 'FooServiceDep';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root', useClass: BarService})
|
||||||
|
class FooService {
|
||||||
|
constructor(public dep: FooServiceDep) {}
|
||||||
|
getMessage() { return 'foo'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should use @Injectable useClass config when token is not provided', () => {
|
||||||
|
let provider: FooService|BarService;
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class App {
|
||||||
|
constructor(service: FooService) { provider = service; }
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(provider !.getMessage()).toBe('bar');
|
||||||
|
|
||||||
|
// ViewEngine incorrectly uses the original class DI config, instead of the one from useClass.
|
||||||
|
if (ivyEnabled) {
|
||||||
|
expect(provider !.dep.name).toBe('BarServiceDep');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use constructor config directly when token is explicitly provided via useClass',
|
||||||
|
() => {
|
||||||
|
let provider: FooService|BarService;
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class App {
|
||||||
|
constructor(service: FooService) { provider = service; }
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{declarations: [App], providers: [{provide: FooService, useClass: FooService}]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(provider !.getMessage()).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should inject correct provider when re-providing an injectable that has useClass', () => {
|
||||||
|
let directProvider: FooService|BarService;
|
||||||
|
let overriddenProvider: FooService|BarService;
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class App {
|
||||||
|
constructor(@Inject('stringToken') overriddenService: FooService, service: FooService) {
|
||||||
|
overriddenProvider = overriddenService;
|
||||||
|
directProvider = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{declarations: [App], providers: [{provide: 'stringToken', useClass: FooService}]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(directProvider !.getMessage()).toBe('bar');
|
||||||
|
expect(overriddenProvider !.getMessage()).toBe('foo');
|
||||||
|
|
||||||
|
// ViewEngine incorrectly uses the original class DI config, instead of the one from useClass.
|
||||||
|
if (ivyEnabled) {
|
||||||
|
expect(directProvider !.dep.name).toBe('BarServiceDep');
|
||||||
|
expect(overriddenProvider !.dep.name).toBe('FooServiceDep');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use constructor config directly when token is explicitly provided as a type provider',
|
||||||
|
() => {
|
||||||
|
let provider: FooService|BarService;
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class App {
|
||||||
|
constructor(service: FooService) { provider = service; }
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App], providers: [FooService]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(provider !.getMessage()).toBe('foo');
|
||||||
|
expect(provider !.dep.name).toBe('FooServiceDep');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('inject', () => {
|
describe('inject', () => {
|
||||||
|
|
||||||
it('should inject from parent view', () => {
|
it('should inject from parent view', () => {
|
||||||
|
|
|
@ -467,10 +467,8 @@ describe('providers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onlyInIvy('VE bug (see FW-1454)')
|
it('should support forward refs in useClass when token is provided', () => {
|
||||||
.it('should support forward refs in useClass when token is provided', () => {
|
@Injectable({providedIn: 'root'})
|
||||||
|
|
||||||
@Injectable({providedIn: 'root', useClass: forwardRef(() => SomeProviderImpl)})
|
|
||||||
abstract class SomeProvider {
|
abstract class SomeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,8 +481,10 @@ describe('providers', () => {
|
||||||
constructor(public foo: SomeProvider) {}
|
constructor(public foo: SomeProvider) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
TestBed.configureTestingModule(
|
TestBed.configureTestingModule({
|
||||||
{declarations: [App], providers: [{provide: SomeProvider, useClass: SomeProvider}]});
|
declarations: [App],
|
||||||
|
providers: [{provide: SomeProvider, useClass: forwardRef(() => SomeProviderImpl)}]
|
||||||
|
});
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(App);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue