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:
crisbeto 2020-02-22 11:18:33 +01:00 committed by Miško Hevery
parent ec789b0198
commit 0bc35a71e2
3 changed files with 130 additions and 22 deletions

View File

@ -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);
} }
} }
} }

View File

@ -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', () => {

View File

@ -467,29 +467,29 @@ 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'})
abstract class SomeProvider {
}
@Injectable({providedIn: 'root', useClass: forwardRef(() => SomeProviderImpl)}) @Injectable()
abstract class SomeProvider { class SomeProviderImpl extends SomeProvider {
} }
@Injectable() @Component({selector: 'my-app', template: ''})
class SomeProviderImpl extends SomeProvider { class App {
} constructor(public foo: SomeProvider) {}
}
@Component({selector: 'my-app', template: ''}) TestBed.configureTestingModule({
class App { declarations: [App],
constructor(public foo: SomeProvider) {} providers: [{provide: SomeProvider, useClass: forwardRef(() => SomeProviderImpl)}]
} });
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
TestBed.configureTestingModule( expect(fixture.componentInstance.foo).toBeAnInstanceOf(SomeProviderImpl);
{declarations: [App], providers: [{provide: SomeProvider, useClass: SomeProvider}]}); });
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.foo).toBeAnInstanceOf(SomeProviderImpl);
});
}); });