-
Notifications
You must be signed in to change notification settings - Fork 27.5k
$inject with ES6 classes can inject the wrong dependencies #15536
Comments
Ensure that a function has its own $inject property, and not an inherited property, when annotating. This ensures that derived functions / classes do not get the dependencies of their super class. Fixes angular#15536
Ensure that a function has its own $inject property, and not an inherited property, when annotating. This ensures that derived functions / classes do not get the dependencies of their super class. Fixes angular#15536
Ensure that a function has its own $inject property, and not an inherited property, when annotating. This ensures that derived functions / classes do not get the dependencies of their super class. Fixes angular#15536
How would this play with derived classes that do not define their own constructor? Wouldn't that fail as it would reject the use of the parent's dependencies? |
Indeed, the attached PR would break this behavior. Personally, I would consider the fact that this behavior works now is more of a curiosity than a feature. Especially so considering that, if transpiled / minified / under strictDi, the derived class will not be given the base class's $inject property and you'll have an app that works in dev but not in prod. |
Thanks for pointing that out, though, it's definitely worth noting. I'm not sure where that note goes. |
I'm pretty sure it does work on production, as I do exactly this where I work (transpiled through Babel). |
Would you mind adding your comment to my PR ( #15538 ) then? Fixing this issue wouldn't necessarily break your workflow, but my PR probably would. |
Sure, done |
Here are some thoughts (please correct me if I am wrong or missing something): Assuming classes
Based on the above, I would say that there is no way to cover all usecases with implicit annotation, so we need to pick one that won't be supported:
Regardless which one we choose not to support, the solution (from the developer's standpoint) will be to explicitly annotate If we keep the current implementation (not supporting (1)), the developer needs to explicitly annotate tl;dr |
@gkalpak let's look at the failure modes. Currently, we silently inject incorrect arguments (for non- If we check YMMV, but I'd take explicit errors over unpredictable runtime behaviour anytime. But maybe we can have our cake and eat it too? If we don't find a On a somewhat unrelated note, looking at the code, it seems to me as if we fail rather surprisingly for classes that don't define an explicit > class Parent { constructor(injected) {} }
> class Child extends Parent { myMethod(a, b) {} }
> Function.prototype.toString.call(Child)
'class Child extends Parent { myMethod(a, b) { } }' ... so AFAICT we'll try to inject |
As mentioned in my previous comment, the problem with the proposed solution is that "constructor-less" derived classes will not be annotated correctly. The result will be the equivalent to: class A { constructor(injected) { this.injected = injected } }
A.$inject = ['injected'];
class B extends A {}
B.$inject = []; // This will be created implicitly. In the above example, there will be no error until something tries to access Unfortunately, both cases are problematic and quite similar in nature (although they fail on different usecases), but I feel the current one is easier to work around. Of cource, if we could find a way to cover all bases, that would be great.
How would we look for a Even if we managed to fix it for native classes, this would still be an issue for transpiled code.
I think this is a known limitation of the current implementation, for lack of a reliable way to solve the issue. There is some discussion in #14175. Just to be clear, I am the first to admit that the current implementation is not ideal and I am open to any sort of improvement. |
Re finding a constructor, currently we fail rather badly... maybe we could:
That's not super robust – if the keyword
WDYT? |
@mprobst I'd rather our implicit annotations logic expected |
Both suggestions SGTM. (And I don't think we would even break any currently working usecases.) |
@mogl so you'd search for the first parenthesis in |
@mprobst Good point. Since we want constructor-less classes to still work when inheriting from another class, we have 2 choices what to do if the first class method is not
I still think we shouldn't look for constructor further in the class stringification as we may break constructor-less classes having a function expression/declaration named Doing that would fix constructor-less classes and would only break the code that works by accident; namely, having the first class method matching the first few argument names with the ones used by class A {
myMethod(dep1, dep2, dep3) {}
constructor(dep1, dep2) {/* code */}
} Since this is quite an edge case and works only by accident, we could get away with doing that in a patch release. WDYT? |
We've diverged a little from the original issue. Going back there, I'd like whatever constructor-detecting heuristic to be used only outside of the Because of that, I agree with @gkalpak's comment that there is no reliable way to detect constructor-less classes and that we should keep current implementation. |
The core issue is that in ES6, classes inherit not only instance properties, but also static properties. This means that static $inject on base class will be seen by the subclasses as well. This behavior can only be observed on ES6 VMs as only a VM can properly set up the prototypes so that static fields get inherited retroactively. In all transpilers the class static fields get copied during class declaration, and so any field which gets added later will not retroactively propagate to the subclasses. What happens here is that AngularJS caches the $inject on class constructor functions. In true VMs the cache becomes visible on subclasses which causes the error described in this document, but when transpiled it works as expected because writing of the $inject on the class constructor after a child class has been declared will not cause $inject propagation to child classes. The fix is to simply to add Here is the test case, and it is what all ES6 VMs will do. (The reason why transpilers don't do this is that it will turn the objects to a slow execution path, and hence its better to be fast then correct in some rarely used corner case.)
|
@mhevery, the problem with that is "constructor-less", derived classes (see #15536 (comment)). class A { constructor(a, b) {} }
A.$inject = ['a', 'b'];
class B extends A {} With |
@gkalpak that is true, but they would not have worked even without |
No, they do work (because they inherit |
@gkalpak but that assumes that someone either decorated the superclass (in which case the should have decorated subclass as well) or that angular cached superclass, before it called subclass. Given all these restrictions I think it is kind of broken already. |
Annotating injectables (either manually or automatically (e.g. via
Annotating injectables without a constructor or (when there are no arguments) was never required and people usually don't do it (even in the Angular codebase we don't annotate injectables that don't have arguments).
Even if less common, this isn't very rare among the usecases that involve extending an injectable (which is what this issue is about). E.g. having a base controller that is used is some templates, but extending that to overwrite one method for a specific template.
As already implied above, I don't think these are real restrictions. tl;dr I still believe that implementing the Therefore I am slightly in favor of either implementing #15536 (comment) or leaving things be. |
I believe I have this issue after upgrading to TypeScript 2.3. I think the issue has to do with the new __extends implementation that includes the Object.setPrototypeOf call. Now my subclassed services are not getting injected properly in dev where ngAnnotate is not active. Original that didn't cause an issue: var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}; New way with the issue: var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})(); |
None of the solutions presented here got enough support to proceed with changing the status quo and the issue has seen no activity lately so I'm closing it. If someone has a concrete proposal that addresses all the mentioned issues we can reopen. |
Note: for support questions, please use one of these channels: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#question. This repository's issues are reserved for feature requests and bug reports.
Do you want to request a feature or report a bug?
Bug
What is the current behavior?
When angular annotates a base class, it uses the inherited annotations when resolving the dependencies of derived classes.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).
http://plnkr.co/edit/55dt9Xx8z5qvHZQ9eDdQ?p=preview
What is the expected behavior?
Derived classes should not inherit their parent's $inject property, as they do not necessarily share dependencies.
What is the motivation / use case for changing the behavior?
This behavior introduces a potentially hard to debug issue, and is probably not what the user expects to happen.
Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.
This is still an issue when running against the snapshot as of 2016-12-21
Other information (e.g. stacktraces, related issues, suggestions how to fix)
The text was updated successfully, but these errors were encountered: