fix(core): infinite loop if injectable using inheritance has a custom decorator (#37022)

If we detect that an injectable class is inheriting from another injectable, we generate code that looks something like this:

```
const baseFactory = ɵɵgetInheritedFactory(Child);

@Injectable()
class Parent {}

@Injectable()
class Child extends Parent {
  static ɵfac = (t) => baseFactory(t || Child)
}
```

This usually works fine, because the `ɵɵgetInheritedFactory` resolves to the factory of `Parent`, but the logic can break down if the `Child` class has a custom decorator. Custom decorators can return a new class that extends the original once, which means that the `ɵɵgetInheritedFactory` call will now resolve to the factory of the `Child`, causing an infinite loop.

These changes fix the issue by changing the inherited factory resolution logic so that it walks up the prototype chain class-by-class, while skipping classes that have the same factory as the class that was passed in.

Fixes #35733.

PR Close #37022
This commit is contained in:
crisbeto 2020-05-09 14:33:54 +02:00 committed by atscott
parent 40f3bb5638
commit ab3f4c9fe1
2 changed files with 146 additions and 12 deletions

View File

@ -657,16 +657,31 @@ export function ɵɵgetFactoryOf<T>(type: Type<any>): FactoryFn<T>|null {
*/ */
export function ɵɵgetInheritedFactory<T>(type: Type<any>): (type: Type<T>) => T { export function ɵɵgetInheritedFactory<T>(type: Type<any>): (type: Type<T>) => T {
return noSideEffects(() => { return noSideEffects(() => {
const proto = Object.getPrototypeOf(type.prototype).constructor as Type<any>; const ownConstructor = type.prototype.constructor;
const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf<T>(proto); const ownFactory = ownConstructor[NG_FACTORY_DEF] || ɵɵgetFactoryOf(ownConstructor);
if (factory !== null) { const objectPrototype = Object.prototype;
return factory; let parent = Object.getPrototypeOf(type.prototype).constructor;
} else {
// There is no factory defined. Either this was improper usage of inheritance // Go up the prototype until we hit `Object`.
// (no Angular decorator on the superclass) or there is no constructor at all while (parent && parent !== objectPrototype) {
// in the inheritance chain. Since the two cases cannot be distinguished, the const factory = parent[NG_FACTORY_DEF] || ɵɵgetFactoryOf(parent);
// latter has to be assumed.
return (t) => new t(); // If we hit something that has a factory and the factory isn't the same as the type,
// we've found the inherited factory. Note the check that the factory isn't the type's
// own factory is redundant in most cases, but if the user has custom decorators on the
// class, this lookup will start one level down in the prototype chain, causing us to
// find the own factory first and potentially triggering an infinite loop downstream.
if (factory && factory !== ownFactory) {
return factory;
}
parent = Object.getPrototypeOf(parent);
} }
// There is no factory defined. Either this was improper usage of inheritance
// (no Angular decorator on the superclass) or there is no constructor at all
// in the inheritance chain. Since the two cases cannot be distinguished, the
// latter has to be assumed.
return t => new t();
}); });
} }

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core'; import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, Type, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core';
import {forwardRef} from '../../src/di/forward_ref'; import {forwardRef} from '../../src/di/forward_ref';
import {createInjector} from '../../src/di/r3_injector'; import {createInjector} from '../../src/di/r3_injector';
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵgetInheritedFactory, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgModuleFactory} from '../../src/render3/ng_module_ref'; import {NgModuleFactory} from '../../src/render3/ng_module_ref';
import {getInjector} from '../../src/render3/util/discovery_utils'; import {getInjector} from '../../src/render3/util/discovery_utils';
@ -1282,7 +1282,126 @@ describe('providers', () => {
expect(injector.get(Some).location).toEqual('From app component'); expect(injector.get(Some).location).toEqual('From app component');
}); });
}); });
// Note: these tests check the behavior of `getInheritedFactory` specifically.
// Since `getInheritedFactory` is only generated in AOT, the tests can't be
// ported directly to TestBed while running in JIT mode.
describe('getInheritedFactory on class with custom decorator', () => {
function addFoo() {
return (constructor: Type<any>): any => {
const decoratedClass = class Extender extends constructor { foo = 'bar'; };
// On IE10 child classes don't inherit static properties by default. If we detect
// such a case, try to account for it so the tests are consistent between browsers.
if (Object.getPrototypeOf(decoratedClass) !== constructor) {
decoratedClass.prototype = constructor.prototype;
}
return decoratedClass;
};
}
it('should find the correct factories if a parent class has a custom decorator', () => {
class GrandParent {
static ɵfac = function GrandParent_Factory() {};
}
@addFoo()
class Parent extends GrandParent {
static ɵfac = function Parent_Factory() {};
}
class Child extends Parent {
static ɵfac = function Child_Factory() {};
}
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
});
it('should find the correct factories if a child class has a custom decorator', () => {
class GrandParent {
static ɵfac = function GrandParent_Factory() {};
}
class Parent extends GrandParent {
static ɵfac = function Parent_Factory() {};
}
@addFoo()
class Child extends Parent {
static ɵfac = function Child_Factory() {};
}
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
});
it('should find the correct factories if a grandparent class has a custom decorator', () => {
@addFoo()
class GrandParent {
static ɵfac = function GrandParent_Factory() {};
}
class Parent extends GrandParent {
static ɵfac = function Parent_Factory() {};
}
class Child extends Parent {
static ɵfac = function Child_Factory() {};
}
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
});
it('should find the correct factories if all classes have a custom decorator', () => {
@addFoo()
class GrandParent {
static ɵfac = function GrandParent_Factory() {};
}
@addFoo()
class Parent extends GrandParent {
static ɵfac = function Parent_Factory() {};
}
@addFoo()
class Child extends Parent {
static ɵfac = function Child_Factory() {};
}
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
});
it('should find the correct factories if parent and grandparent classes have a custom decorator',
() => {
@addFoo()
class GrandParent {
static ɵfac = function GrandParent_Factory() {};
}
@addFoo()
class Parent extends GrandParent {
static ɵfac = function Parent_Factory() {};
}
class Child extends Parent {
static ɵfac = function Child_Factory() {};
}
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
});
});
}); });
interface ComponentTest { interface ComponentTest {
providers?: Provider[]; providers?: Provider[];
viewProviders?: Provider[]; viewProviders?: Provider[];