Skip to content

Commit ab3f4c9

Browse files
crisbetoatscott
authored andcommitted
fix(core): infinite loop if injectable using inheritance has a custom decorator (angular#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 angular#35733. PR Close angular#37022
1 parent 40f3bb5 commit ab3f4c9

File tree

2 files changed

+146
-12
lines changed

2 files changed

+146
-12
lines changed

packages/core/src/render3/di.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -657,16 +657,31 @@ export function ɵɵgetFactoryOf<T>(type: Type<any>): FactoryFn<T>|null {
657657
*/
658658
export function ɵɵgetInheritedFactory<T>(type: Type<any>): (type: Type<T>) => T {
659659
return noSideEffects(() => {
660-
const proto = Object.getPrototypeOf(type.prototype).constructor as Type<any>;
661-
const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf<T>(proto);
662-
if (factory !== null) {
663-
return factory;
664-
} else {
665-
// There is no factory defined. Either this was improper usage of inheritance
666-
// (no Angular decorator on the superclass) or there is no constructor at all
667-
// in the inheritance chain. Since the two cases cannot be distinguished, the
668-
// latter has to be assumed.
669-
return (t) => new t();
660+
const ownConstructor = type.prototype.constructor;
661+
const ownFactory = ownConstructor[NG_FACTORY_DEF] || ɵɵgetFactoryOf(ownConstructor);
662+
const objectPrototype = Object.prototype;
663+
let parent = Object.getPrototypeOf(type.prototype).constructor;
664+
665+
// Go up the prototype until we hit `Object`.
666+
while (parent && parent !== objectPrototype) {
667+
const factory = parent[NG_FACTORY_DEF] || ɵɵgetFactoryOf(parent);
668+
669+
// If we hit something that has a factory and the factory isn't the same as the type,
670+
// we've found the inherited factory. Note the check that the factory isn't the type's
671+
// own factory is redundant in most cases, but if the user has custom decorators on the
672+
// class, this lookup will start one level down in the prototype chain, causing us to
673+
// find the own factory first and potentially triggering an infinite loop downstream.
674+
if (factory && factory !== ownFactory) {
675+
return factory;
676+
}
677+
678+
parent = Object.getPrototypeOf(parent);
670679
}
680+
681+
// There is no factory defined. Either this was improper usage of inheritance
682+
// (no Angular decorator on the superclass) or there is no constructor at all
683+
// in the inheritance chain. Since the two cases cannot be distinguished, the
684+
// latter has to be assumed.
685+
return t => new t();
671686
});
672687
}

packages/core/test/render3/providers_spec.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core';
9+
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';
1010
import {forwardRef} from '../../src/di/forward_ref';
1111
import {createInjector} from '../../src/di/r3_injector';
12-
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
12+
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵgetInheritedFactory, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
1313
import {RenderFlags} from '../../src/render3/interfaces/definition';
1414
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
1515
import {getInjector} from '../../src/render3/util/discovery_utils';
@@ -1282,7 +1282,126 @@ describe('providers', () => {
12821282
expect(injector.get(Some).location).toEqual('From app component');
12831283
});
12841284
});
1285+
1286+
// Note: these tests check the behavior of `getInheritedFactory` specifically.
1287+
// Since `getInheritedFactory` is only generated in AOT, the tests can't be
1288+
// ported directly to TestBed while running in JIT mode.
1289+
describe('getInheritedFactory on class with custom decorator', () => {
1290+
function addFoo() {
1291+
return (constructor: Type<any>): any => {
1292+
const decoratedClass = class Extender extends constructor { foo = 'bar'; };
1293+
1294+
// On IE10 child classes don't inherit static properties by default. If we detect
1295+
// such a case, try to account for it so the tests are consistent between browsers.
1296+
if (Object.getPrototypeOf(decoratedClass) !== constructor) {
1297+
decoratedClass.prototype = constructor.prototype;
1298+
}
1299+
1300+
return decoratedClass;
1301+
};
1302+
}
1303+
1304+
it('should find the correct factories if a parent class has a custom decorator', () => {
1305+
class GrandParent {
1306+
static ɵfac = function GrandParent_Factory() {};
1307+
}
1308+
1309+
@addFoo()
1310+
class Parent extends GrandParent {
1311+
static ɵfac = function Parent_Factory() {};
1312+
}
1313+
1314+
class Child extends Parent {
1315+
static ɵfac = function Child_Factory() {};
1316+
}
1317+
1318+
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
1319+
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
1320+
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
1321+
});
1322+
1323+
it('should find the correct factories if a child class has a custom decorator', () => {
1324+
class GrandParent {
1325+
static ɵfac = function GrandParent_Factory() {};
1326+
}
1327+
1328+
class Parent extends GrandParent {
1329+
static ɵfac = function Parent_Factory() {};
1330+
}
1331+
1332+
@addFoo()
1333+
class Child extends Parent {
1334+
static ɵfac = function Child_Factory() {};
1335+
}
1336+
1337+
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
1338+
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
1339+
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
1340+
});
1341+
1342+
it('should find the correct factories if a grandparent class has a custom decorator', () => {
1343+
@addFoo()
1344+
class GrandParent {
1345+
static ɵfac = function GrandParent_Factory() {};
1346+
}
1347+
1348+
class Parent extends GrandParent {
1349+
static ɵfac = function Parent_Factory() {};
1350+
}
1351+
1352+
class Child extends Parent {
1353+
static ɵfac = function Child_Factory() {};
1354+
}
1355+
1356+
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
1357+
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
1358+
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
1359+
});
1360+
1361+
it('should find the correct factories if all classes have a custom decorator', () => {
1362+
@addFoo()
1363+
class GrandParent {
1364+
static ɵfac = function GrandParent_Factory() {};
1365+
}
1366+
1367+
@addFoo()
1368+
class Parent extends GrandParent {
1369+
static ɵfac = function Parent_Factory() {};
1370+
}
1371+
1372+
@addFoo()
1373+
class Child extends Parent {
1374+
static ɵfac = function Child_Factory() {};
1375+
}
1376+
1377+
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
1378+
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
1379+
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
1380+
});
1381+
1382+
it('should find the correct factories if parent and grandparent classes have a custom decorator',
1383+
() => {
1384+
@addFoo()
1385+
class GrandParent {
1386+
static ɵfac = function GrandParent_Factory() {};
1387+
}
1388+
1389+
@addFoo()
1390+
class Parent extends GrandParent {
1391+
static ɵfac = function Parent_Factory() {};
1392+
}
1393+
1394+
class Child extends Parent {
1395+
static ɵfac = function Child_Factory() {};
1396+
}
1397+
1398+
expect(ɵɵgetInheritedFactory(Child).name).toBe('Parent_Factory');
1399+
expect(ɵɵgetInheritedFactory(Parent).name).toBe('GrandParent_Factory');
1400+
expect(ɵɵgetInheritedFactory(GrandParent).name).toBeFalsy();
1401+
});
1402+
});
12851403
});
1404+
12861405
interface ComponentTest {
12871406
providers?: Provider[];
12881407
viewProviders?: Provider[];

0 commit comments

Comments
 (0)