feat(eslint-plugin): [inject-at-top] add rule requiring inject() at the top of the class#3009
feat(eslint-plugin): [inject-at-top] add rule requiring inject() at the top of the class#3009JamesHenry merged 5 commits intoangular-eslint:mainangular-eslint/angular-eslint:mainfrom AdiMarianMutu:feat/inject-at-topAdiMarianMutu/angular-eslint:feat/inject-at-topCopy head branch name to clipboard
Conversation
JamesHenry
left a comment
There was a problem hiding this comment.
Thanks for this @AdiMarianMutu, there's a lot of syntactic constructs that this currently does not cover:
foo1 = cond ? inject(A) : inject(B); // ConditionalExpression
foo2 = inject(A, {optional: true}) ?? fallback; // LogicalExpression
foo3 = inject(A, {optional: true})!; // TSNonNullExpression
foo4 = inject(A) as Foo; // TSAsExpression
foo5 = wrap(inject(A)); // wrapper CallExpression
foo6 = new Wrapper(inject(A)); // NewExpression
foo7 = [inject(A), inject(B)]; // ArrayExpression
foo8 = {svc: inject(A)}; // ObjectExpression
foo9 = (sideEffect(), inject(A)); // SequenceExpression
foo10 = `${inject(A).id}`; // TemplateLiteral
foo11 = (() => inject(A))(); // IIFE — still runs at field initSee https://stackblitz.com/edit/4vsytrav?file=src%2Fmain.ts for a variant of the angular.dev playground that compiles and runs with these applied
|
@AdiMarianMutu Thanks for continuing your work on this. Please rebase on the v22 changes and be sure to add |
…he top of the class Class fields are initialized in the order they are written, so any field, getter, or method declared above an inject() call cannot safely use the injected service: it will still be undefined at the moment its initializer runs. TypeScript catches the obvious shape of this bug (TS2729) when an earlier field reads a later field directly, but it does not trace through a getter body or a method call, so any indirection hides the problem from the compiler and the failure only shows up at runtime, with a "Cannot read properties of undefined" error that points at the consumer rather than at the misordered declaration. This rule flags every inject() call in a class decorated with @component, @directive, @Injectable or @pipe that is declared below any other (non-static) member, forcing all injections to sit at the top of the class and eliminating the ordering hazard. Enabled in the recommended config.
… hide an eager inject() call Adds coverage for every syntactic construct raised in the PR review: ConditionalExpression, LogicalExpression, TSNonNullExpression, TSAsExpression, wrapping CallExpression, NewExpression, ArrayExpression, ObjectExpression, SequenceExpression, TemplateLiteral, and IIFE. Each shape is tested in both directions (eager inject() reported when below another member, ignored when declared at the top), plus laziness discriminators (arrow / function-expression / method bodies that should remain ignored, including arrows passed as call arguments rather than invoked).
65d547d to
97ee150
Compare
Done, let me know if I've missed something. |
|
View your CI Pipeline Execution ↗ for commit 0bb65e1
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3009 +/- ##
=======================================
Coverage 96.70% 96.71%
=======================================
Files 215 217 +2
Lines 4982 5023 +41
Branches 1581 1611 +30
=======================================
+ Hits 4818 4858 +40
- Misses 160 161 +1
Partials 4 4
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Thank you @AdiMarianMutu |
Note
This fixes #3008
Class fields are initialized in the order they are written, so any field, getter, or method declared above an inject() call cannot safely use the injected service: it will still be undefined at the moment its initializer runs.
TypeScript catches the obvious shape of this bug (TS2729) when an earlier field reads a later field directly, but it does not trace through a getter body or a method call, so any indirection hides the problem from the compiler and the failure only shows up at runtime, with a "Cannot read properties of undefined" error that points at the consumer rather than at the misordered declaration.
This rule flags every inject() call in a class decorated with @component, @directive, @Injectable or @pipe that is declared below any other (non-static) member, forcing all injections to sit at the top of the class and eliminating the ordering hazard.
Enabled in the recommended config.