Repository: angular/angular (TypeScript)
Date: 2020-06-12 20:02:28+00:00
Task type: Bug Fix

Issue:
Too much recursion because of @Injectable conflicts with other decorators
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅

Oh hi there! 😄

To expedite issue processing please search open and closed issues before submitting a new one.
Existing issues often contain information about workarounds, resolution, or progress updates.

🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->


# 🐞 bug report

### Affected Package
Looks like `@angular/core`


### Is this a regression?
Yes, the versions before 9 work correct (https://stackblitz.com/edit/angular-3sutb9 - works, because stackblitz is not provided Ivy-rendering)


### Description
`@injectable` conflicts with other decorators in this example:
```
@Injectable()
export class TestServiceA {}

@Injectable()
@testDecorator()
export class TestServiceB extends TestServiceA {}

@Injectable()
export class TestService extends TestServiceB {}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  providers: [TestService]
})
export class AppComponent  {
  constructor(private testService: TestService) {}
}
```


## 🔬 Minimal Reproduction
You can just clone the repo and run locally (stackblitz doesn't work with last versions of Angular)
https://github.com/tamtakoe/angular-max-call-stack-size

## 🔥 Exception or Error
<pre><code>core.js:3866 ERROR RangeError: Maximum call stack size exceeded
    at TestServiceB_Factory (app.component.ts:30) ...

or  ERROR InternalError: "too much recursion" ... in Firefox etc.
</code></pre>


## 🌍  Your Environment

**Angular Version:**
<pre><code>Angular CLI: 9.0.3
Node: 12.4.0
OS: darwin x64

Angular: 9.0.4
... common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.3
@angular-devkit/build-angular     0.900.3
@angular-devkit/build-optimizer   0.900.3
@angular-devkit/build-webpack     0.900.3
@angular-devkit/core              9.0.3
@angular-devkit/schematics        9.0.3
@angular/cli                      9.0.3
@ngtools/webpack                  9.0.3
@schematics/angular               9.0.3
@schematics/update                0.900.3
rxjs                              6.5.4
typescript                        3.7.5
webpack                           4.41.2
</code></pre>



Issue discussion:
I have a similar error after upgrading to 9.0.4/0.900.4:

```
Compiling @angular/… : es2015 as esm2015

chunk {} runtime.689ba4fd6cadb82c1ac2.js (runtime) 1.45 kB [entry] [rendered]
chunk {1} main.9d65be46ac259c9df33b.js (main) 700 kB [initial] [rendered]
chunk {2} polyfills.da2c6c4849ef9aea9de5.js (polyfills) 35.6 kB [initial] [rendered]
chunk {3} styles.1b30f95df1bf800225e4.css (styles) 62.7 kB [initial] [rendered]
Date: 2020-02-28T12:40:47.476Z - Hash: 5d47462af19da00a8547 - Time: 94639ms

ERROR in Error when flattening the source-map "<path1>.d.ts.map" for "<path1>.d.ts": RangeError: Maximum call stack size exceeded
ERROR in Error when flattening the source-map "<path2>.d.ts.map" for "<path2>.d.ts": RangeError: Maximum call stack size exceeded
ERROR in Error when flattening the source-map "<path3>.d.ts.map" for "<path3>.d.ts": RangeError: Maximum call stack size exceeded
ERROR in Error when flattening the source-map "<path4>.d.ts.map" for "<path4>.d.ts": RangeError: Maximum call stack size exceeded
…
```

Unfortunately, I was not yet able to reproduce this issue.
This appears to happen only once after upgrading an existing Angular <9.0.4/3 (not sure which one) project to 9.0.3/4. I can run `ng build` again and then it works flawlessly. Even with `npm ci` I cannot reproduce the error. :/
This seems to be a legit report of the behaviour change in ivy with custom decorators (there were multiple similar bugs reported previously), here is a live repro: https://ng-run.com/edit/IneGzHSECoI3LtdEXPWh
There's several issues here.

First, this line introduces the infinite loop:

```ts
newConstructor.prototype = Object.create(target.prototype);
```

This effectively sets up `target` to act as a superclass of `newConstructor`, as far as I understand it. Then, when the runtime looks up the factory for `TestServiceB` it delegates to the parent (as there's no own constructor) here:

https://github.com/angular/angular/blob/d2c60cc216982ee018e7033f7f62f2b423f2d289/packages/core/src/render3/di.ts#L660

Here, `type.prototype === newConstructor.prototype`, so using `Object.getPrototypeOf` we arrive at `target.prototype` which is `TestServiceB`. Therefore, the superclass' factory that is obtained for `TestServiceB` *is* `TestServiceB` again, causing the infinite loop.

**Disclaimer**: I am always confused by prototype stuff in JS, so there could be nonsense in the above.

---

Secondly, the compiled definitions that are present as static property on the class are not copied over to `newConstructor`. This is a problem for DI to work correctly, as factory functions etc will not be available. You don't currently see the effect of this as none of the constructors have parameters that need to be injected.

Solution patch:
diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts
--- a/packages/core/src/render3/di.ts
+++ b/packages/core/src/render3/di.ts
@@ -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 {
   return noSideEffects(() => {
-    const proto = Object.getPrototypeOf(type.prototype).constructor as Type<any>;
-    const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf<T>(proto);
-    if (factory !== null) {
-      return factory;
-    } else {
-      // 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();
+    const ownConstructor = type.prototype.constructor;
+    const ownFactory = ownConstructor[NG_FACTORY_DEF] || ɵɵgetFactoryOf(ownConstructor);
+    const objectPrototype = Object.prototype;
+    let parent = Object.getPrototypeOf(type.prototype).constructor;
+
+    // Go up the prototype until we hit `Object`.
+    while (parent && parent !== objectPrototype) {
+      const factory = parent[NG_FACTORY_DEF] || ɵɵgetFactoryOf(parent);
+
+      // 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();
   });
 }


Test patch:
diff --git a/packages/core/test/render3/providers_spec.ts b/packages/core/test/render3/providers_spec.ts
--- a/packages/core/test/render3/providers_spec.ts
+++ b/packages/core/test/render3/providers_spec.ts
@@ -6,10 +6,10 @@
  * 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 {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 {NgModuleFactory} from '../../src/render3/ng_module_ref';
 import {getInjector} from '../../src/render3/util/discovery_utils';
@@ -1282,7 +1282,126 @@ describe('providers', () => {
       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 {
   providers?: Provider[];
   viewProviders?: Provider[];