Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(eslint-plugin): [inject-at-top] add rule requiring inject() at the top of the class#3009

Merged
JamesHenry merged 5 commits into
angular-eslint:mainangular-eslint/angular-eslint:mainfrom
AdiMarianMutu:feat/inject-at-topAdiMarianMutu/angular-eslint:feat/inject-at-topCopy head branch name to clipboard
Jul 12, 2026
Merged

feat(eslint-plugin): [inject-at-top] add rule requiring inject() at the top of the class#3009
JamesHenry merged 5 commits into
angular-eslint:mainangular-eslint/angular-eslint:mainfrom
AdiMarianMutu:feat/inject-at-topAdiMarianMutu/angular-eslint:feat/inject-at-topCopy head branch name to clipboard

Conversation

@AdiMarianMutu

@AdiMarianMutu AdiMarianMutu commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

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.

@JamesHenry JamesHenry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 init

See https://stackblitz.com/edit/4vsytrav?file=src%2Fmain.ts for a variant of the angular.dev playground that compiles and runs with these applied

Comment thread packages/eslint-plugin/src/rules/inject-at-top.ts Outdated
@AdiMarianMutu
AdiMarianMutu requested a review from JamesHenry May 16, 2026 11:30
@JamesHenry

Copy link
Copy Markdown
Member

@AdiMarianMutu Thanks for continuing your work on this.

Please rebase on the v22 changes and be sure to add Service support in here now that that is supported in v22, then we'll look to get this merged. We can bed this in for a while and potentially consider recommended status for v23

…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).
@AdiMarianMutu

Copy link
Copy Markdown
Contributor Author

@AdiMarianMutu Thanks for continuing your work on this.

Please rebase on the v22 changes and be sure to add Service support in here now that that is supported in v22, then we'll look to get this merged. We can bed this in for a while and potentially consider recommended status for v23

Done, let me know if I've missed something.

@nx-cloud

nx-cloud Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 0bb65e1

Command Status Duration Result
nx run-many -t test --configuration ci ✅ Succeeded 13s View ↗
nx run-many -t build typecheck test check-rule-... ✅ Succeeded 3m 55s View ↗
nx-cloud record -- pnpm nx sync:check ✅ Succeeded 6s View ↗
nx-cloud record -- pnpm format-check ✅ Succeeded 6s View ↗
nx run-many -t test ✅ Succeeded 29s View ↗
nx run-many -t build ✅ Succeeded 16s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-12 16:18:53 UTC

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.56098% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.71%. Comparing base (8acfa92) to head (0bb65e1).

Files with missing lines Patch % Lines
packages/eslint-plugin/src/rules/inject-at-top.ts 97.36% 1 Missing ⚠️
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           
Flag Coverage Δ
unittest 96.71% <97.56%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/eslint-plugin/tests/rules/inject-at-top/cases.ts 100.00% <100.00%> (ø)
packages/eslint-plugin/src/rules/inject-at-top.ts 97.36% <97.36%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JamesHenry
JamesHenry merged commit eccf00d into angular-eslint:main Jul 12, 2026
8 checks passed
@JamesHenry

Copy link
Copy Markdown
Member

Thank you @AdiMarianMutu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[new rule] enforce inject() at the top of the class to prevent runtime bugs

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.