fix(core): take @Host into account while processing `useFactory` arguments (#40122)

DI providers can be defined via `useFactory` function, which may have arguments configured via `deps` array.
The `deps` array may contain DI flags represented by DI decorators (such as `@Self`, `@SkipSelf`, etc). Prior to this
commit, having the `@Host` decorator in `deps` array resulted in runtime error in Ivy. The problem was that the `@Host`
decorator was not taken into account while `useFactory` argument list was constructed, the `@Host` decorator was
treated as a token that should be looked up.

This commit updates the logic which prepares `useFactory` arguments to recognize the `@Host` decorator.

PR Close #40122
This commit is contained in:
Andrew Kushnir 2020-12-14 23:07:29 -08:00 committed by Joey Perrott
parent 212245f197
commit 3735633bb0
6 changed files with 114 additions and 2 deletions

View File

@ -3,7 +3,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1485, "runtime-es2015": 1485,
"main-es2015": 140333, "main-es2015": 140871,
"polyfills-es2015": 36964 "polyfills-es2015": 36964
} }
} }

View File

@ -11,13 +11,14 @@ import '../util/ng_dev_mode';
import {AbstractType, Type} from '../interface/type'; import {AbstractType, Type} from '../interface/type';
import {getClosureSafeProperty} from '../util/property'; import {getClosureSafeProperty} from '../util/property';
import {stringify} from '../util/stringify'; import {stringify} from '../util/stringify';
import {resolveForwardRef} from './forward_ref'; import {resolveForwardRef} from './forward_ref';
import {getInjectImplementation, injectRootLimpMode} from './inject_switch'; import {getInjectImplementation, injectRootLimpMode} from './inject_switch';
import {InjectionToken} from './injection_token'; import {InjectionToken} from './injection_token';
import {Injector} from './injector'; import {Injector} from './injector';
import {InjectFlags} from './interface/injector'; import {InjectFlags} from './interface/injector';
import {ValueProvider} from './interface/provider'; import {ValueProvider} from './interface/provider';
import {Inject, Optional, Self, SkipSelf} from './metadata'; import {Host, Inject, Optional, Self, SkipSelf} from './metadata';
const _THROW_IF_NOT_FOUND = {}; const _THROW_IF_NOT_FOUND = {};
@ -151,6 +152,8 @@ export function injectArgs(types: (Type<any>|InjectionToken<any>|any[])[]): any[
flags |= InjectFlags.SkipSelf; flags |= InjectFlags.SkipSelf;
} else if (meta instanceof Self || meta.ngMetadataName === 'Self' || meta === Self) { } else if (meta instanceof Self || meta.ngMetadataName === 'Self' || meta === Self) {
flags |= InjectFlags.Self; flags |= InjectFlags.Self;
} else if (meta instanceof Host || meta.ngMetadataName === 'Host' || meta === Host) {
flags |= InjectFlags.Host;
} else if (meta instanceof Inject || meta === Inject) { } else if (meta instanceof Inject || meta === Inject) {
type = meta.token; type = meta.token;
} else { } else {

View File

@ -2803,6 +2803,106 @@ describe('di', () => {
}); });
}); });
it('should be able to use Host in `useFactory` dependency config', () => {
// Scenario:
// ---------
// <root (provides token A)>
// <comp (provides token B via useFactory(@Host() @Inject(A))></comp>
// </root>
@Component({
selector: 'root',
template: '<comp></comp>',
viewProviders: [{
provide: 'A',
useValue: 'A from Root',
}]
})
class Root {
}
@Component({
selector: 'comp',
template: '{{ token }}',
viewProviders: [{
provide: 'B',
deps: [[new Inject('A'), new Host()]],
useFactory: (token: string) => `${token} (processed by useFactory)`,
}]
})
class Comp {
constructor(@Inject('B') readonly token: string) {}
}
@Component({
template: `<root></root>`,
})
class App {
}
TestBed.configureTestingModule({declarations: [Root, Comp, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('A from Root (processed by useFactory)');
});
it('should not lookup outside of the host element when Host is used in `useFactory`', () => {
// Scenario:
// ---------
// <root (provides token A)>
// <intermediate>
// <comp (provides token B via useFactory(@Host() @Inject(A))></comp>
// </intermediate>
// </root>
@Component({
selector: 'root',
template: '<intermediate></intermediate>',
viewProviders: [{
provide: 'A',
useValue: 'A from Root',
}]
})
class Root {
}
@Component({
selector: 'intermediate',
template: '<comp></comp>',
})
class Intermediate {
}
@Component({
selector: 'comp',
template: '{{ token }}',
viewProviders: [{
provide: 'B',
deps: [[new Inject('A'), new Host(), new Optional()]],
useFactory: (token: string) =>
token ? `${token} (processed by useFactory)` : 'No token A found',
}]
})
class Comp {
constructor(@Inject('B') readonly token: string) {}
}
@Component({
template: `<root></root>`,
})
class App {
}
TestBed.configureTestingModule({declarations: [Root, Comp, App, Intermediate]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
// Making sure that the `@Host` takes effect and token `A` becomes unavailable in DI since it's
// defined one level up from the Comp's host view.
expect(fixture.nativeElement.textContent).toBe('No token A found');
});
it('should not cause cyclic dependency if same token is requested in deps with @SkipSelf', () => { it('should not cause cyclic dependency if same token is requested in deps with @SkipSelf', () => {
@Component({ @Component({
selector: 'my-comp', selector: 'my-comp',

View File

@ -236,6 +236,9 @@
{ {
"name": "FormsModule" "name": "FormsModule"
}, },
{
"name": "Host"
},
{ {
"name": "INJECTOR" "name": "INJECTOR"
}, },

View File

@ -5,6 +5,9 @@
{ {
"name": "EMPTY_ARRAY" "name": "EMPTY_ARRAY"
}, },
{
"name": "Host"
},
{ {
"name": "INJECTOR" "name": "INJECTOR"
}, },

View File

@ -290,6 +290,9 @@
{ {
"name": "HashLocationStrategy" "name": "HashLocationStrategy"
}, },
{
"name": "Host"
},
{ {
"name": "INITIAL_VALUE" "name": "INITIAL_VALUE"
}, },