eslint-config-agent 3.0.5 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +15 -0
- package/configs/length-rule-scope.js +27 -0
- package/configs/overrides.js +2 -13
- package/configs/typescript.js +23 -0
- package/exports/recommended.d.ts +5 -2
- package/exports/recommended.js +34 -5
- package/index.js +289 -0
- package/package.json +3 -2
- package/rules/plugin/n/index.js +14 -0
- package/rules/plugin/react/index.js +81 -2
- package/rules/plugin/typescript-eslint/index.js +202 -0
- package/rules/require-spec-file-tsx/helpers.js +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to this project will be documented in this file. See [Conven
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
## [3.1.0](https://github.com/tupe12334/eslint-config-agent/compare/v3.0.5...v3.1.0) (2026-07-02)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **config:** enable @typescript-eslint/explicit-function-return-type for .ts ([#257](https://github.com/tupe12334/eslint-config-agent/issues/257)) ([7469e06](https://github.com/tupe12334/eslint-config-agent/commit/7469e06503157f838071cbdaa8216065ccce9975)), closes [#255](https://github.com/tupe12334/eslint-config-agent/issues/255)
|
|
12
|
+
* **config:** enable @typescript-eslint/method-signature-style in the shared rule set ([#313](https://github.com/tupe12334/eslint-config-agent/issues/313)) ([e261d06](https://github.com/tupe12334/eslint-config-agent/commit/e261d06e8724702fb763a11e24f922d51156a66c))
|
|
13
|
+
* **config:** enable @typescript-eslint/no-misused-promises in the shared rule set ([#285](https://github.com/tupe12334/eslint-config-agent/issues/285)) ([0fba9f0](https://github.com/tupe12334/eslint-config-agent/commit/0fba9f0ecabe89368cb1ffa6dcd2c6e983815472))
|
|
14
|
+
* **config:** enable @typescript-eslint/no-mixed-enums in the shared rule set ([#317](https://github.com/tupe12334/eslint-config-agent/issues/317)) ([51ee52b](https://github.com/tupe12334/eslint-config-agent/commit/51ee52b991516730db6bb3a9c5014a3a780af911))
|
|
15
|
+
* **config:** enable @typescript-eslint/no-unnecessary-condition in the shared rule set ([#352](https://github.com/tupe12334/eslint-config-agent/issues/352)) ([d8449f5](https://github.com/tupe12334/eslint-config-agent/commit/d8449f57600a60afd37e35a171e772371583219b)), closes [#351](https://github.com/tupe12334/eslint-config-agent/issues/351)
|
|
16
|
+
* **config:** enable @typescript-eslint/no-use-before-define in the shared rule set ([#338](https://github.com/tupe12334/eslint-config-agent/issues/338)) ([4079061](https://github.com/tupe12334/eslint-config-agent/commit/407906131981054bddb05fbfc531bf54839484e9)), closes [#337](https://github.com/tupe12334/eslint-config-agent/issues/337)
|
|
17
|
+
* **config:** enable @typescript-eslint/prefer-enum-initializers ([#289](https://github.com/tupe12334/eslint-config-agent/issues/289)) ([630f36f](https://github.com/tupe12334/eslint-config-agent/commit/630f36fa6c79b8cbfed1187cb4192b7118b786dd))
|
|
18
|
+
* **config:** enable @typescript-eslint/prefer-optional-chain for .ts/.tsx files ([#260](https://github.com/tupe12334/eslint-config-agent/issues/260)) ([45d6423](https://github.com/tupe12334/eslint-config-agent/commit/45d6423d18a4778aed6f5391c8fe12f02b1f31e2))
|
|
19
|
+
* **config:** enable consistent-return in the shared rule set ([#331](https://github.com/tupe12334/eslint-config-agent/issues/331)) ([4f7f522](https://github.com/tupe12334/eslint-config-agent/commit/4f7f5221e17b031efd2ca1a96202ca40de8ad88b))
|
|
20
|
+
* **config:** enable logical-assignment-operators in the shared rule set ([#295](https://github.com/tupe12334/eslint-config-agent/issues/295)) ([5cec4f8](https://github.com/tupe12334/eslint-config-agent/commit/5cec4f87a32817fa005488deb39fb773a09c4690))
|
|
21
|
+
* **config:** enable n/prefer-node-protocol in the shared rule set ([#291](https://github.com/tupe12334/eslint-config-agent/issues/291)) ([cc8c47a](https://github.com/tupe12334/eslint-config-agent/commit/cc8c47a621f5d71f5847d35d3ce69f4f9c043070))
|
|
22
|
+
* **config:** enable no-alert in the shared rule set ([#297](https://github.com/tupe12334/eslint-config-agent/issues/297)) ([f7cc412](https://github.com/tupe12334/eslint-config-agent/commit/f7cc4129f40e59669f284e0f683186992f4bb937))
|
|
23
|
+
* **config:** enable no-constant-binary-expression in the shared rule set ([#354](https://github.com/tupe12334/eslint-config-agent/issues/354)) ([310deb6](https://github.com/tupe12334/eslint-config-agent/commit/310deb6b0f08c0872b810a0c4c31020b34603352))
|
|
24
|
+
* **config:** enable no-constructor-return in the shared rule set ([#332](https://github.com/tupe12334/eslint-config-agent/issues/332)) ([79357a3](https://github.com/tupe12334/eslint-config-agent/commit/79357a3a699c6bd66287c2f8a02d66ee7bf93716))
|
|
25
|
+
* **config:** enable no-debugger in the shared rule set ([#254](https://github.com/tupe12334/eslint-config-agent/issues/254)) ([d941604](https://github.com/tupe12334/eslint-config-agent/commit/d941604ecb43ba02cd570af9d49f3f7861791f39))
|
|
26
|
+
* **config:** enable no-extend-native in the shared rule set ([#330](https://github.com/tupe12334/eslint-config-agent/issues/330)) ([138006b](https://github.com/tupe12334/eslint-config-agent/commit/138006b480e69752e16e2c80f34c675ab0b9c594))
|
|
27
|
+
* **config:** enable no-new-wrappers in the shared rule set ([#125](https://github.com/tupe12334/eslint-config-agent/issues/125)) ([2cdc1c2](https://github.com/tupe12334/eslint-config-agent/commit/2cdc1c2143b288c38e41cbad0c2267522b6d912d)), closes [#124](https://github.com/tupe12334/eslint-config-agent/issues/124)
|
|
28
|
+
* **config:** enable no-script-url in the shared rule set ([#251](https://github.com/tupe12334/eslint-config-agent/issues/251)) ([18147fc](https://github.com/tupe12334/eslint-config-agent/commit/18147fc5c5a6e37177f8c1c0599250a0b816c255)), closes [#250](https://github.com/tupe12334/eslint-config-agent/issues/250)
|
|
29
|
+
* **config:** enable no-sequences in the shared rule set ([#267](https://github.com/tupe12334/eslint-config-agent/issues/267)) ([6d45085](https://github.com/tupe12334/eslint-config-agent/commit/6d450854eac3cba29f6fb6715fd4477954c48666)), closes [#81](https://github.com/tupe12334/eslint-config-agent/issues/81)
|
|
30
|
+
* **config:** enable no-undef-init in the shared rule set ([#344](https://github.com/tupe12334/eslint-config-agent/issues/344)) ([8521b73](https://github.com/tupe12334/eslint-config-agent/commit/8521b73062acd82e608088dcb2541dde509fb5b0)), closes [#343](https://github.com/tupe12334/eslint-config-agent/issues/343)
|
|
31
|
+
* **config:** enable no-unmodified-loop-condition in the shared rule set ([#96](https://github.com/tupe12334/eslint-config-agent/issues/96)) ([acd21b7](https://github.com/tupe12334/eslint-config-agent/commit/acd21b7940ea8b675f207da464d198c631a9ddde)), closes [#95](https://github.com/tupe12334/eslint-config-agent/issues/95)
|
|
32
|
+
* **config:** enable no-unreachable-loop in the shared rule set ([#274](https://github.com/tupe12334/eslint-config-agent/issues/274)) ([a18fcf2](https://github.com/tupe12334/eslint-config-agent/commit/a18fcf271a05ec9aa3865b220feaf23b59257863)), closes [#87](https://github.com/tupe12334/eslint-config-agent/issues/87)
|
|
33
|
+
* **config:** enable no-useless-call in the shared rule set ([#301](https://github.com/tupe12334/eslint-config-agent/issues/301)) ([3edf7aa](https://github.com/tupe12334/eslint-config-agent/commit/3edf7aaf1e553342892ef42382bb8acc7e8ab37e))
|
|
34
|
+
* **config:** enable no-useless-constructor in the shared rule set ([#312](https://github.com/tupe12334/eslint-config-agent/issues/312)) ([76808e2](https://github.com/tupe12334/eslint-config-agent/commit/76808e2ee038d5ba6094b75e6bc0ecb456a279ae)), closes [#311](https://github.com/tupe12334/eslint-config-agent/issues/311)
|
|
35
|
+
* **config:** enable no-useless-rename in the shared rule set ([#336](https://github.com/tupe12334/eslint-config-agent/issues/336)) ([f1fa2e4](https://github.com/tupe12334/eslint-config-agent/commit/f1fa2e41a4fd467b601c194b3f860c4c50c3848e)), closes [#113](https://github.com/tupe12334/eslint-config-agent/issues/113)
|
|
36
|
+
* **config:** enable prefer-named-capture-group in the shared rule set ([#308](https://github.com/tupe12334/eslint-config-agent/issues/308)) ([75877f9](https://github.com/tupe12334/eslint-config-agent/commit/75877f9bde9614a3c311343f11a5b55fbf31cb28))
|
|
37
|
+
* **config:** enable react/jsx-no-useless-fragment in the shared rule set ([#272](https://github.com/tupe12334/eslint-config-agent/issues/272)) ([de076fc](https://github.com/tupe12334/eslint-config-agent/commit/de076fc52107878dca23cfa491ef19a48a99be6d)), closes [#271](https://github.com/tupe12334/eslint-config-agent/issues/271)
|
|
38
|
+
* **config:** enable react/self-closing-comp in the shared rule set ([#269](https://github.com/tupe12334/eslint-config-agent/issues/269)) ([20d15b6](https://github.com/tupe12334/eslint-config-agent/commit/20d15b6891ea88df2df2b31d75b0a70ce1b88587))
|
|
39
|
+
* **config:** enable yoda rule in the shared rule set ([#273](https://github.com/tupe12334/eslint-config-agent/issues/273)) ([b7ebd29](https://github.com/tupe12334/eslint-config-agent/commit/b7ebd29351475549581c72ec09f55dc968abcf54)), closes [#61](https://github.com/tupe12334/eslint-config-agent/issues/61)
|
|
40
|
+
* **react:** enable react/hook-use-state in the shared rule set ([#319](https://github.com/tupe12334/eslint-config-agent/issues/319)) ([bb23979](https://github.com/tupe12334/eslint-config-agent/commit/bb23979d479e0af64b4eba550b30a7c6aa2bf7bb))
|
|
41
|
+
* **react:** enable react/jsx-key in the shared rule set ([#342](https://github.com/tupe12334/eslint-config-agent/issues/342)) ([297c706](https://github.com/tupe12334/eslint-config-agent/commit/297c7067cdb1ed9db5a2065e53b60885420dbddb))
|
|
42
|
+
* **react:** enable react/no-object-type-as-default-prop in the shared rule set ([#323](https://github.com/tupe12334/eslint-config-agent/issues/323)) ([4d43920](https://github.com/tupe12334/eslint-config-agent/commit/4d439201b343cf9aebb3c702a2d99b74b723ab71))
|
|
43
|
+
|
|
44
|
+
### Bug Fixes
|
|
45
|
+
|
|
46
|
+
* **ci:** publish directly from scheduled-release.yml ([#356](https://github.com/tupe12334/eslint-config-agent/issues/356)) ([ac2db7b](https://github.com/tupe12334/eslint-config-agent/commit/ac2db7b9e911d58fd422e0d5461a8f4b1ec0efea))
|
|
47
|
+
* **config:** disable prefer-nullish-coalescing to resolve ?? contradiction ([#321](https://github.com/tupe12334/eslint-config-agent/issues/321)) ([13a18d4](https://github.com/tupe12334/eslint-config-agent/commit/13a18d48068448289f36698a7e000f4252ef309a))
|
|
48
|
+
* publish rule helper modules, not just index.js ([#270](https://github.com/tupe12334/eslint-config-agent/issues/270)) ([4e81453](https://github.com/tupe12334/eslint-config-agent/commit/4e81453914fc4e1bd8ca048ba03054a272b168e3))
|
|
49
|
+
* **recommended:** downgrade max-lines rules from error to warn ([#335](https://github.com/tupe12334/eslint-config-agent/issues/335)) ([b69ccb3](https://github.com/tupe12334/eslint-config-agent/commit/b69ccb34ede4f7ede1a49d72b1e35ab52d2e9023)), closes [#85](https://github.com/tupe12334/eslint-config-agent/issues/85)
|
|
50
|
+
* **recommended:** keep process.env property guard in the relaxed preset ([#281](https://github.com/tupe12334/eslint-config-agent/issues/281)) ([a99f00c](https://github.com/tupe12334/eslint-config-agent/commit/a99f00c13639c191650c8c7f15df3e84bd969b46))
|
|
51
|
+
* **ts:** disable no-throw-literal for TS files; let only-throw-error handle it ([#299](https://github.com/tupe12334/eslint-config-agent/issues/299)) ([c0c9b28](https://github.com/tupe12334/eslint-config-agent/commit/c0c9b28afc2436573e90b87316415569f3e44ff7))
|
|
52
|
+
|
|
7
53
|
## [3.0.5](https://github.com/tupe12334/eslint-config-agent/compare/v3.0.4...v3.0.5) (2026-06-24)
|
|
8
54
|
|
|
9
55
|
### Features
|
package/README.md
CHANGED
|
@@ -483,6 +483,14 @@ This ESLint configuration prioritizes **explicit code** over convenient shortcut
|
|
|
483
483
|
`default` that omits `break` silently falls through into the cases below it.
|
|
484
484
|
Pinning `default` to the end keeps its order-independent meaning legible. Not
|
|
485
485
|
auto-fixable: moving a clause that omits `break` could change behavior.
|
|
486
|
+
- **`consistent-return`**: Requires every `return` statement in a function to
|
|
487
|
+
either always specify a value or never specify one. A function that returns
|
|
488
|
+
a value on one branch and falls through (or hits a bare `return;`) on another
|
|
489
|
+
silently yields `undefined` on the unhandled paths — a quiet,
|
|
490
|
+
plausible-but-wrong mistake the type checker does not reliably catch, since
|
|
491
|
+
an inferred `T | undefined` return type checks cleanly either way. Not
|
|
492
|
+
auto-fixable: only the author knows whether the missing branch should return
|
|
493
|
+
a value or the value-returning branch should stop returning one.
|
|
486
494
|
- **`no-extra-bind`**: Forbids `.bind()` on a function that never references
|
|
487
495
|
`this` (and binds no arguments) — `(() => x).bind(obj)`,
|
|
488
496
|
`function () { return 1 }.bind(this)`, `handler.bind(this)` where `handler`
|
|
@@ -540,6 +548,13 @@ This ESLint configuration prioritizes **explicit code** over convenient shortcut
|
|
|
540
548
|
`verbatimModuleSyntax` / `isolatedModules`. Splitting type and value
|
|
541
549
|
re-exports keeps the emitted module graph honest and a barrel file's
|
|
542
550
|
value-vs-type surface legible. Auto-fixable.
|
|
551
|
+
- **`no-useless-rename`**: Forbids renaming an import, export, or destructured
|
|
552
|
+
binding to the exact name it already has — `import { foo as foo } from
|
|
553
|
+
'./foo'`, `export { bar as bar }`, `const { baz: baz } = obj`. The `as`/`:`
|
|
554
|
+
clause reads as if it transforms the binding, so a reader stops to compare
|
|
555
|
+
both sides only to discover they are identical: pure punctuation noise that
|
|
556
|
+
adds nothing. Exactly the ceremony an AI assistant spells out mechanically
|
|
557
|
+
for an alias it never needed. Auto-fixable with `eslint --fix`.
|
|
543
558
|
|
|
544
559
|
### Bundled Custom Rules
|
|
545
560
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function/File Length Rule Scope
|
|
3
|
+
*
|
|
4
|
+
* `files`/`ignores` scope shared by the strict (error-level)
|
|
5
|
+
* `max-lines-per-function`/`max-lines` block in `configs/overrides.js` and the
|
|
6
|
+
* `recommended` preset's warn-level equivalent (`exports/recommended.js`).
|
|
7
|
+
* Extracted to its own module so the two can never drift apart — the preset
|
|
8
|
+
* only needs to relax the *severity*, not widen or narrow which files the
|
|
9
|
+
* rules apply to (tests, stories, config files, and this package's own
|
|
10
|
+
* `index.js` stay exempt either way).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const lengthRuleFileMatch = {
|
|
14
|
+
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
15
|
+
ignores: [
|
|
16
|
+
'**/*.stories.{js,jsx,ts,tsx}',
|
|
17
|
+
'**/*.test.{js,jsx,ts,tsx}',
|
|
18
|
+
'**/*.spec.{js,jsx,ts,tsx}',
|
|
19
|
+
'**/test/**/*.{js,jsx,ts,tsx}',
|
|
20
|
+
'**/tests/**/*.{js,jsx,ts,tsx}',
|
|
21
|
+
'**/__tests__/**/*.{js,jsx,ts,tsx}',
|
|
22
|
+
'**/configs/**/*.{js,ts}',
|
|
23
|
+
'*.config.{js,ts}',
|
|
24
|
+
'eslint.config.{js,ts}',
|
|
25
|
+
'index.js',
|
|
26
|
+
],
|
|
27
|
+
}
|
package/configs/overrides.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import globals from 'globals'
|
|
9
9
|
import allRules from '../rules/index.js'
|
|
10
|
+
import { lengthRuleFileMatch } from './length-rule-scope.js'
|
|
10
11
|
|
|
11
12
|
export const overridesConfig = (
|
|
12
13
|
sharedRestrictedSyntax,
|
|
@@ -87,19 +88,7 @@ export const overridesConfig = (
|
|
|
87
88
|
|
|
88
89
|
// Function and file length rules - strict error thresholds
|
|
89
90
|
{
|
|
90
|
-
|
|
91
|
-
ignores: [
|
|
92
|
-
'**/*.stories.{js,jsx,ts,tsx}',
|
|
93
|
-
'**/*.test.{js,jsx,ts,tsx}',
|
|
94
|
-
'**/*.spec.{js,jsx,ts,tsx}',
|
|
95
|
-
'**/test/**/*.{js,jsx,ts,tsx}',
|
|
96
|
-
'**/tests/**/*.{js,jsx,ts,tsx}',
|
|
97
|
-
'**/__tests__/**/*.{js,jsx,ts,tsx}',
|
|
98
|
-
'**/configs/**/*.{js,ts}',
|
|
99
|
-
'*.config.{js,ts}',
|
|
100
|
-
'eslint.config.{js,ts}',
|
|
101
|
-
'index.js',
|
|
102
|
-
],
|
|
91
|
+
...lengthRuleFileMatch,
|
|
103
92
|
rules: {
|
|
104
93
|
'max-lines-per-function': allRules.maxFunctionLinesError,
|
|
105
94
|
'max-lines': allRules.maxFileLinesError,
|
package/configs/typescript.js
CHANGED
|
@@ -58,6 +58,29 @@ export const typescriptConfig = (
|
|
|
58
58
|
},
|
|
59
59
|
],
|
|
60
60
|
'custom/no-default-class-export': 'error',
|
|
61
|
+
// Require an explicit return type on every function, not just the
|
|
62
|
+
// exported ones `explicit-module-boundary-types` would cover. Inferred
|
|
63
|
+
// return types are computed bottom-up from the body, so a refactor deep
|
|
64
|
+
// inside a helper can silently widen or change what it returns — a `null`
|
|
65
|
+
// branch added here, a `Promise` introduced there — and nothing flags it
|
|
66
|
+
// until that drift surfaces far away at a call site (or type-checks fine
|
|
67
|
+
// and ships as a runtime bug). Writing the type down inverts the check:
|
|
68
|
+
// the function body is now verified against the contract the author
|
|
69
|
+
// declared, so the mistake fails at the function itself instead of
|
|
70
|
+
// leaking outward. This is exactly the shortcut an AI assistant takes
|
|
71
|
+
// when it emits a helper and lets inference "figure out" the return type,
|
|
72
|
+
// which puts it squarely in this config's explicit-over-clever,
|
|
73
|
+
// fail-locally stance. It is type-aware but cheap (it reads the function
|
|
74
|
+
// signature, no whole-program analysis) and is in neither
|
|
75
|
+
// `eslint:recommended` nor typescript-eslint's `strictTypeChecked` /
|
|
76
|
+
// `stylisticTypeChecked` presets this config extends, so it must be
|
|
77
|
+
// enabled explicitly — which is why `tools-view` re-adds it by hand on
|
|
78
|
+
// top of the base config. It is scoped to `.ts`/`.mts`/`.cts` (this
|
|
79
|
+
// file's `files`) and deliberately NOT applied to `.tsx`: requiring an
|
|
80
|
+
// explicit return type on every JSX component is high-noise for little
|
|
81
|
+
// gain, so React function components stay exempt, matching how
|
|
82
|
+
// `tools-view` enables it on `src/**/*.ts` only.
|
|
83
|
+
'@typescript-eslint/explicit-function-return-type': 'error',
|
|
61
84
|
'single-export/single-export': 'error',
|
|
62
85
|
'required-exports/required-exports': [
|
|
63
86
|
'error',
|
package/exports/recommended.d.ts
CHANGED
|
@@ -3,8 +3,11 @@ import type { Linter } from 'eslint'
|
|
|
3
3
|
/**
|
|
4
4
|
* Recommended (relaxed) preset: the default config with the most divisive rules
|
|
5
5
|
* turned off (spec files, single export, custom `error/*`, optional chaining /
|
|
6
|
-
* nullish-coalescing bans, and so on) for incremental adoption.
|
|
7
|
-
*
|
|
6
|
+
* nullish-coalescing bans, and so on) for incremental adoption.
|
|
7
|
+
* `max-lines-per-function`/`max-lines` are downgraded to `warn` (same 70/100
|
|
8
|
+
* line thresholds as the strict default) so a real codebase's legacy long
|
|
9
|
+
* functions/files surface as backlog instead of failing CI. Every other
|
|
10
|
+
* surviving rule still reports as an error.
|
|
8
11
|
*/
|
|
9
12
|
declare const config: Linter.Config[]
|
|
10
13
|
|
package/exports/recommended.js
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import config from '../index.js'
|
|
27
|
+
import { noProcessEnvironmentPropertiesConfig } from '../rules/no-process-env-properties/index.js'
|
|
28
|
+
import { errorOptions as maxFunctionLinesErrorOptions } from '../rules/max-function-lines/index.js'
|
|
29
|
+
import { errorOptions as maxFileLinesErrorOptions } from '../rules/max-file-lines/index.js'
|
|
30
|
+
import { lengthRuleFileMatch } from '../configs/length-rule-scope.js'
|
|
27
31
|
|
|
28
32
|
const relaxedOverrides = {
|
|
29
33
|
name: 'eslint-config-agent/recommended-overrides',
|
|
@@ -71,11 +75,36 @@ const relaxedOverrides = {
|
|
|
71
75
|
// adopt incrementally in any React/Preact + Tailwind codebase — by far the
|
|
72
76
|
// most common modern setup — so relax it for the recommended preset.
|
|
73
77
|
'jsx-classname/require-classname': 'off',
|
|
74
|
-
// The most divisive layer:
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
+
// The most divisive layer: optional chaining (`?.`), nullish coalescing
|
|
79
|
+
// (`??`), type assertions, switch-default bans, and inline union/record
|
|
80
|
+
// type restrictions. Removing them lets idiomatic TypeScript through during
|
|
81
|
+
// adoption. The process.env guard (`no-process-env-properties`) is NOT in
|
|
82
|
+
// this group: scattered `process.env.FOO` access is a configuration smell
|
|
83
|
+
// regardless of adoption stage, so it is kept as the one surviving
|
|
84
|
+
// no-restricted-syntax check.
|
|
85
|
+
'no-restricted-syntax': ['error', noProcessEnvironmentPropertiesConfig],
|
|
78
86
|
},
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
// The strict default promotes function/file length to hard errors in its
|
|
90
|
+
// final override layer (`configs/overrides.js`'s "Function and file length
|
|
91
|
+
// rules" block) — at >70 lines per function and >100 lines per file — which
|
|
92
|
+
// blocks incremental adoption: an existing codebase pointing `recommended` at
|
|
93
|
+
// its real source tree still gets fatal errors on every long legacy
|
|
94
|
+
// function/file, exactly the "wall of errors" this preset exists to avoid
|
|
95
|
+
// (see issue #85). Downgrade both to `warn` while keeping the same 70/100
|
|
96
|
+
// thresholds — and the same file scope (`lengthRuleFileMatch`, shared with
|
|
97
|
+
// the strict block) — so the signal still surfaces in `eslint` output and the
|
|
98
|
+
// backlog can be burned down, but it no longer fails a CI run that treats
|
|
99
|
+
// errors as fatal. Consumers can re-enable these as errors with their own
|
|
100
|
+
// override layer once they are ready.
|
|
101
|
+
const lengthWarningOverrides = {
|
|
102
|
+
name: 'eslint-config-agent/recommended-length-warnings',
|
|
103
|
+
...lengthRuleFileMatch,
|
|
104
|
+
rules: {
|
|
105
|
+
'max-lines-per-function': ['warn', maxFunctionLinesErrorOptions],
|
|
106
|
+
'max-lines': ['warn', maxFileLinesErrorOptions],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default [...config, relaxedOverrides, lengthWarningOverrides]
|
package/index.js
CHANGED
|
@@ -26,6 +26,19 @@ const sharedRules = {
|
|
|
26
26
|
...allRules.pluginRules,
|
|
27
27
|
'object-curly-newline': 'off',
|
|
28
28
|
'no-shadow': 'off',
|
|
29
|
+
// The core `no-use-before-define` rule is intentionally left `off`: it
|
|
30
|
+
// false-positives on TypeScript type/interface declarations and enum
|
|
31
|
+
// members referenced before their textual position, which are hoisted and
|
|
32
|
+
// safe. `@typescript-eslint/no-use-before-define` below is the documented
|
|
33
|
+
// replacement — see that rule's comment for the full rationale.
|
|
34
|
+
'no-use-before-define': 'off',
|
|
35
|
+
// The core `no-unused-private-class-members` rule is intentionally left
|
|
36
|
+
// `off`: it only understands ECMAScript `#hashPrivate` fields, not
|
|
37
|
+
// TypeScript's `private` keyword, which is the actual dead-code surface in
|
|
38
|
+
// this codebase. `@typescript-eslint/no-unused-private-class-members` is
|
|
39
|
+
// the documented replacement — see that rule's comment for the full
|
|
40
|
+
// rationale.
|
|
41
|
+
'no-unused-private-class-members': 'off',
|
|
29
42
|
'comma-dangle': 'off',
|
|
30
43
|
'function-paren-newline': 'off',
|
|
31
44
|
quotes: 'off',
|
|
@@ -84,6 +97,20 @@ const sharedRules = {
|
|
|
84
97
|
// no-await-in-loop`. It is not in `eslint:recommended`, so it is enabled
|
|
85
98
|
// explicitly here.
|
|
86
99
|
'no-await-in-loop': 'error',
|
|
100
|
+
// Disallow loop conditions that reference a variable that is never modified
|
|
101
|
+
// inside the loop body. A loop whose condition tests a value that nothing
|
|
102
|
+
// inside the body ever updates is almost always a bug: either an unintended
|
|
103
|
+
// infinite loop or a forgotten increment / mutation. TypeScript does not
|
|
104
|
+
// catch this because the code is well-typed — the variable simply keeps its
|
|
105
|
+
// initial value and the condition never changes truth-value. This is exactly
|
|
106
|
+
// the kind of quiet, wrong-behavior bug AI assistants emit when they
|
|
107
|
+
// scaffold a loop body and lose track of what actually advances it. It is
|
|
108
|
+
// not in `eslint:recommended`, so it must be enabled explicitly here. The
|
|
109
|
+
// rule has a very low false-positive rate (genuine cases where an outer scope
|
|
110
|
+
// change is intended are rare and easy to suppress with a comment) and is not
|
|
111
|
+
// auto-fixable because only the author knows which variable was meant to
|
|
112
|
+
// change.
|
|
113
|
+
'no-unmodified-loop-condition': 'error',
|
|
87
114
|
// Disallow an `else`/`else if` block when the preceding `if` already exits
|
|
88
115
|
// the function via `return`. The `else` is dead weight: once the `if` branch
|
|
89
116
|
// returns, the code after it is unreachable from that path, so wrapping the
|
|
@@ -116,6 +143,22 @@ const sharedRules = {
|
|
|
116
143
|
// behind. The rule is auto-fixable, so consumers can adopt it with
|
|
117
144
|
// `eslint --fix`.
|
|
118
145
|
'no-useless-return': 'error',
|
|
146
|
+
// Require every `return` statement in a function to either always specify a
|
|
147
|
+
// value or never specify one. A function that returns a value on one branch
|
|
148
|
+
// and falls through — or hits a bare `return;` — on another silently yields
|
|
149
|
+
// `undefined` on the unhandled paths, and the caller then receives
|
|
150
|
+
// `undefined` where it expected the value. The bug surfaces far downstream as
|
|
151
|
+
// a "cannot read property of undefined" crash rather than at the offending
|
|
152
|
+
// branch, and the type checker does not reliably catch it: an inferred
|
|
153
|
+
// `T | undefined` return type type-checks cleanly even though the
|
|
154
|
+
// fall-through was accidental. This is exactly the quiet, plausible-but-wrong
|
|
155
|
+
// control-flow mistake an AI assistant emits when it adds an early-exit
|
|
156
|
+
// branch and forgets to carry a value through it. Not in `eslint:recommended`,
|
|
157
|
+
// so it must be enabled explicitly here. The rule is intentionally not
|
|
158
|
+
// auto-fixable — only the author knows whether the missing branch should
|
|
159
|
+
// return a value or the value-returning branch should stop returning one —
|
|
160
|
+
// so adoption surfaces the call sites rather than silently rewriting them.
|
|
161
|
+
'consistent-return': 'error',
|
|
119
162
|
// Disallow a value assigned to a variable that is never read before the
|
|
120
163
|
// variable is overwritten or its scope ends — a "dead store". Writing
|
|
121
164
|
// `let total = compute()` and then unconditionally reassigning `total = 0`
|
|
@@ -181,6 +224,17 @@ const sharedRules = {
|
|
|
181
224
|
// prevent. Enforcing it in the shared config means consumers no longer have
|
|
182
225
|
// to re-add it on top of the package.
|
|
183
226
|
eqeqeq: ['error', 'always'],
|
|
227
|
+
// Require the natural reading order in comparisons: the variable (or
|
|
228
|
+
// expression) on the left, the literal on the right — `count === 0`, never
|
|
229
|
+
// `0 === count`. The reversed "Yoda" form exists only to guard against the
|
|
230
|
+
// assignment typo `if (count = 0)`, but `eqeqeq` (directly above) already
|
|
231
|
+
// forces `===`/`!==` everywhere, and TypeScript flags accidental assignment
|
|
232
|
+
// in a condition as a type error, so the Yoda workaround is pure readability
|
|
233
|
+
// tax. `exceptRange` keeps the clearer range idiom (`0 <= x && x < limit`)
|
|
234
|
+
// which reads as a natural number-line comparison and is harder to express in
|
|
235
|
+
// the canonical order. The rule is auto-fixable, so consumers can adopt it
|
|
236
|
+
// with `eslint --fix`.
|
|
237
|
+
yoda: ['error', 'never', { exceptRange: true }],
|
|
184
238
|
// Disallow reassigning function parameters and mutating their properties.
|
|
185
239
|
// Reassigning a parameter decouples it from the caller's argument and hides the
|
|
186
240
|
// function's real inputs; mutating a parameter's properties causes
|
|
@@ -275,6 +329,30 @@ const sharedRules = {
|
|
|
275
329
|
// with `eslint --fix`; it is not in `eslint:recommended`, so it is enabled
|
|
276
330
|
// explicitly here.
|
|
277
331
|
'no-extra-bind': 'error',
|
|
332
|
+
// Disallow `.call()` and `.apply()` when the first argument (the `this`
|
|
333
|
+
// binding) is `undefined` or `null` and the result is identical to a
|
|
334
|
+
// direct call. `fn.call(null, a, b)` behaves exactly like `fn(a, b)` in
|
|
335
|
+
// non-strict mode and almost always in strict mode: the wrapper only adds
|
|
336
|
+
// noise and an extra property lookup. `fn.apply(undefined, [a, b])` is the
|
|
337
|
+
// same deadweight applied to a spread. The two are the direct-invocation
|
|
338
|
+
// counterpart of the `no-extra-bind` rule just above, which catches
|
|
339
|
+
// `.bind()` that never changes `this` — together they close the "look up
|
|
340
|
+
// the prototype chain to call the function" footgun an AI assistant emits
|
|
341
|
+
// when it wants to forward arguments or detach a method without realizing
|
|
342
|
+
// a direct call suffices. The rule is auto-fixable (`eslint --fix`) and is
|
|
343
|
+
// not in `eslint:recommended`, so it is enabled explicitly here.
|
|
344
|
+
'no-useless-call': 'error',
|
|
345
|
+
// Disallow renaming an import, export, or destructured binding to the exact
|
|
346
|
+
// name it already has — `import { foo as foo } from './foo'`,
|
|
347
|
+
// `export { bar as bar }`, `const { baz: baz } = obj`. The `as`/`:` clause
|
|
348
|
+
// reads as if it transforms the binding, so a reader stops to compare both
|
|
349
|
+
// sides only to discover they are identical: pure punctuation noise that
|
|
350
|
+
// adds nothing. It is the binding-alias sibling of `no-useless-call` just
|
|
351
|
+
// above — both look meaningful and are dead — and exactly the ceremony an
|
|
352
|
+
// AI assistant spells out mechanically for an alias it never needed. The
|
|
353
|
+
// rule is auto-fixable (`eslint --fix`) and is not in `eslint:recommended`,
|
|
354
|
+
// so it is enabled explicitly here.
|
|
355
|
+
'no-useless-rename': 'error',
|
|
278
356
|
// Require `Object.hasOwn(obj, key)` over the legacy ways of asking the same
|
|
279
357
|
// question. The two forms it replaces are each a footgun: calling
|
|
280
358
|
// `obj.hasOwnProperty(key)` directly breaks the moment `obj` has a `null`
|
|
@@ -299,6 +377,18 @@ const sharedRules = {
|
|
|
299
377
|
// explicit-over-clever, AI-safety stance. It is also the foundation the
|
|
300
378
|
// `prefer-const` rule builds on. The rule is auto-fixable.
|
|
301
379
|
'no-var': 'error',
|
|
380
|
+
// Disallow explicitly initializing a variable to `undefined` —
|
|
381
|
+
// `let value = undefined` instead of the equivalent, shorter `let value`.
|
|
382
|
+
// Every declared-but-unassigned binding is already `undefined`, so the
|
|
383
|
+
// initializer adds a token that looks like it is doing something (choosing
|
|
384
|
+
// a starting value) while actually doing nothing; a reader has to stop and
|
|
385
|
+
// confirm that `undefined` really is the intended value rather than a
|
|
386
|
+
// half-finished assignment the author forgot to fill in. It is exactly the
|
|
387
|
+
// no-op an AI assistant emits when it scaffolds a variable before deciding
|
|
388
|
+
// what to assign to it. Not in `eslint:recommended`, so it must be enabled
|
|
389
|
+
// explicitly here. The rule is auto-fixable (`eslint --fix` deletes the
|
|
390
|
+
// redundant initializer), so adoption costs nothing.
|
|
391
|
+
'no-undef-init': 'error',
|
|
302
392
|
// Require object literal shorthand for properties and methods, so
|
|
303
393
|
// `{ value: value }` collapses to `{ value }` and `{ run: function () {} }`
|
|
304
394
|
// to `{ run() {} }`. The longhand forms carry no extra meaning — they are
|
|
@@ -324,6 +414,32 @@ const sharedRules = {
|
|
|
324
414
|
// also exactly what an AI assistant trained on older code reaches for. The
|
|
325
415
|
// rule is auto-fixable, so consumers can adopt it with `eslint --fix`.
|
|
326
416
|
'operator-assignment': ['error', 'always'],
|
|
417
|
+
// Require the logical-assignment shorthand (`x ||= y`, `x &&= y`, `x ??= y`)
|
|
418
|
+
// wherever a binding is reassigned by a logical expression over itself. A
|
|
419
|
+
// longhand `x = x || defaultValue` names the target twice — once as the left
|
|
420
|
+
// operand of `||` and once as the assignment destination — which is the same
|
|
421
|
+
// duplicated-name typo site the `operator-assignment` rule directly above
|
|
422
|
+
// already catches for arithmetic operators. Writing `x ||= defaultValue`
|
|
423
|
+
// removes the second spelling of the name entirely so the assignment-by-typo
|
|
424
|
+
// footgun disappears, and it states the intent — "set this variable only if it
|
|
425
|
+
// is currently falsy / nullish / truthy" — in fewer characters and with the
|
|
426
|
+
// exact same semantics. The three forms map directly onto the boolean and
|
|
427
|
+
// nullish operators the config already steers code toward (`eqeqeq`,
|
|
428
|
+
// `@typescript-eslint/prefer-nullish-coalescing`): `&&=` for "assign only when
|
|
429
|
+
// truthy", `||=` for "assign only when falsy", `??=` for "assign only when null
|
|
430
|
+
// or undefined". `enforceForIfStatements: true` extends the check to the
|
|
431
|
+
// equivalent `if`-guarded form (`if (!x) x = y`, `if (x == null) x = y`), so
|
|
432
|
+
// both the expression and the statement spelling of the pattern are caught. The
|
|
433
|
+
// longhand `x = x || y` is also exactly what an AI assistant trained on
|
|
434
|
+
// pre-ES2021 code reaches for, which puts it squarely in this config's
|
|
435
|
+
// AI-safety scope alongside `operator-assignment`. The rule is auto-fixable
|
|
436
|
+
// (`eslint --fix`), so adoption is free, and it is not in `eslint:recommended`,
|
|
437
|
+
// so it must be enabled explicitly here.
|
|
438
|
+
'logical-assignment-operators': [
|
|
439
|
+
'error',
|
|
440
|
+
'always',
|
|
441
|
+
{ enforceForIfStatements: true },
|
|
442
|
+
],
|
|
327
443
|
// Disallow chained assignment expressions such as `a = b = c = 0`. Chaining
|
|
328
444
|
// collapses several writes into one expression: the value flows right-to-left
|
|
329
445
|
// through bindings that have nothing to do with each other, so a reader has to
|
|
@@ -361,6 +477,21 @@ const sharedRules = {
|
|
|
361
477
|
// explicit-over-clever, AI-safety scope. The rule is auto-fixable, so
|
|
362
478
|
// consumers can adopt it with `eslint --fix`.
|
|
363
479
|
'no-object-constructor': 'error',
|
|
480
|
+
// Disallow the primitive-wrapper constructors `new String()`, `new Number()`,
|
|
481
|
+
// and `new Boolean()`. These do the opposite of what they look like: instead of
|
|
482
|
+
// converting to a primitive they produce a **boxed object**, so
|
|
483
|
+
// `typeof new Number(5)` is `'object'` (not `'number'`),
|
|
484
|
+
// `new Boolean(false)` is always truthy (the object is truthy), and
|
|
485
|
+
// `new String('x') === 'x'` is `false`. The caller typically expects a
|
|
486
|
+
// primitive and the wrong type is a quiet, wrong-result trap that the type
|
|
487
|
+
// checker will not always catch when the boxed type is widened to `object`.
|
|
488
|
+
// For conversion use the unadorned call form (`Number(str)`, `String(n)`,
|
|
489
|
+
// `Boolean(x)`) — that is what `no-implicit-coercion` already pushes code
|
|
490
|
+
// toward, and these two rules are complementary: `no-implicit-coercion` bans
|
|
491
|
+
// the shorthand coercions (`!!x`, `+str`), `no-new-wrappers` bans the
|
|
492
|
+
// constructor path to the same wrong object. The rule is auto-fixable, so
|
|
493
|
+
// consumers can adopt it with `eslint --fix`.
|
|
494
|
+
'no-new-wrappers': 'error',
|
|
364
495
|
// Require object spread (`{ ...a, ...b }`) over `Object.assign({}, a, b)` when
|
|
365
496
|
// the call is building a brand-new object (its first argument is an object
|
|
366
497
|
// literal). The `Object.assign({}, ...)` form is a more indirect, punctuation-
|
|
@@ -392,6 +523,25 @@ const sharedRules = {
|
|
|
392
523
|
'error',
|
|
393
524
|
{ allowImplicit: false, checkForEach: true },
|
|
394
525
|
],
|
|
526
|
+
// Disallow a loop whose body always exits on the first iteration — a `for`,
|
|
527
|
+
// `while`, or `do...while` where every control-flow path ends in `return`,
|
|
528
|
+
// `break`, or `throw`. Such a loop can never reach a second pass: it reads
|
|
529
|
+
// as "iterate every element" but actually runs at most once, collapsing what
|
|
530
|
+
// the author probably meant as a search into a single-element check. The
|
|
531
|
+
// classic form is a misplaced `return` inside a `for-of`:
|
|
532
|
+
// `for (const item of list) { if (cond(item)) return item; return null }`
|
|
533
|
+
// The unconditional `return null` exits on the first iteration regardless of
|
|
534
|
+
// the condition, so every element after the first is silently ignored. This
|
|
535
|
+
// is a wrong-result correctness bug that type checking cannot catch (the
|
|
536
|
+
// types are fine; only the data flow is broken) and exactly the shape AI
|
|
537
|
+
// assistants emit when they flatten a "find the first match" pattern into a
|
|
538
|
+
// loop and lose track of which exit belongs where. It is the loop-level
|
|
539
|
+
// sibling of `array-callback-return` directly above: both catch the "loop
|
|
540
|
+
// body was supposed to visit every element, but silently doesn't" class of
|
|
541
|
+
// mistake. The rule is not in `eslint:recommended`, so it is enabled
|
|
542
|
+
// explicitly. It is not auto-fixable because only the author knows whether
|
|
543
|
+
// the loop or the misplaced exit is wrong.
|
|
544
|
+
'no-unreachable-loop': 'error',
|
|
395
545
|
// Only allow throwing real `Error` objects — reject `throw 'boom'`,
|
|
396
546
|
// `throw { code: 500 }`, `throw 42` and any other non-Error value. A thrown
|
|
397
547
|
// string or plain object carries no stack trace, so the failure surfaces with
|
|
@@ -423,6 +573,26 @@ const sharedRules = {
|
|
|
423
573
|
// keeps the check legible. The rule is not auto-fixable because only the
|
|
424
574
|
// author knows which operand was meant.
|
|
425
575
|
'no-self-compare': 'error',
|
|
576
|
+
// Disallow a binary or logical expression whose result is provably
|
|
577
|
+
// constant regardless of a variable operand: a `&&`/`||` whose left side
|
|
578
|
+
// is already a statically known boolean (`true || sideEffect()`, `false
|
|
579
|
+
// && sideEffect()` — the right side, and any side effect in it, never
|
|
580
|
+
// runs, yet reads as if it were conditional), a loose-equality check
|
|
581
|
+
// against `null`/`undefined` where the other side can never be nullish
|
|
582
|
+
// (`null == undefined` — always `true`), or a `===`/`!==` comparison
|
|
583
|
+
// between two freshly constructed objects (`new Error() === new
|
|
584
|
+
// Error()` — two distinct object references can never be `===`-equal, so
|
|
585
|
+
// the comparison always evaluates the same way no matter what either
|
|
586
|
+
// constructor does). Each of these type-checks and runs without error, so
|
|
587
|
+
// the constant result hides behind what reads like a real, live
|
|
588
|
+
// condition. This is exactly the family `no-self-compare` and
|
|
589
|
+
// `no-unsafe-optional-chaining` above already guard against: a condition
|
|
590
|
+
// that looks meaningful but is defeated by its own operands, and
|
|
591
|
+
// precisely the copy-paste slip an AI assistant leaves behind when it
|
|
592
|
+
// stubs out a condition with a literal or duplicates a `new` expression on
|
|
593
|
+
// both sides of a comparison. Not auto-fixable: only the author knows
|
|
594
|
+
// which operand was meant.
|
|
595
|
+
'no-constant-binary-expression': 'error',
|
|
426
596
|
// Disallow optional chaining in positions where a short-circuit to
|
|
427
597
|
// `undefined` is immediately used in a way that throws at runtime — member
|
|
428
598
|
// access, a call, arithmetic, spread, `instanceof`, `in`, or destructuring on
|
|
@@ -473,6 +643,24 @@ const sharedRules = {
|
|
|
473
643
|
// and not the `Function` constructor). The rule is not auto-fixable because
|
|
474
644
|
// only the author can restate the string body as real code.
|
|
475
645
|
'no-new-func': 'error',
|
|
646
|
+
// Disallow monkey-patching the prototype of a built-in object —
|
|
647
|
+
// `Array.prototype.last = function () { ... }`,
|
|
648
|
+
// `Object.prototype.keysOf = function () { ... }`, `String.prototype.foo = ...`.
|
|
649
|
+
// The patch mutates global state every module and dependency shares: the
|
|
650
|
+
// effect is invisible at the call site, order-dependent (whoever loads first
|
|
651
|
+
// wins, so two libraries patching the same prototype clash silently), and an
|
|
652
|
+
// enumerable addition to `Object.prototype` / `Array.prototype` leaks into
|
|
653
|
+
// every `for...in` loop and `in` check in the program, quietly breaking
|
|
654
|
+
// unrelated code. A safe, explicit equivalent always exists — a standalone
|
|
655
|
+
// helper (`last(arr)`) or a local wrapper — so the patch buys a little
|
|
656
|
+
// call-site sugar at the cost of a global hazard, and it is exactly the
|
|
657
|
+
// shortcut an AI assistant reaches for when asked to "add a method to
|
|
658
|
+
// arrays." This is the prototype-mutation sibling of the `no-new-func` ban
|
|
659
|
+
// directly above. It is not in `eslint:recommended`, so it must be enabled
|
|
660
|
+
// explicitly. Not auto-fixable: only the author can move the behavior into a
|
|
661
|
+
// standalone function. Extending a user-defined class is unaffected — the
|
|
662
|
+
// rule only flags built-in globals.
|
|
663
|
+
'no-extend-native': 'error',
|
|
476
664
|
// Disallow `eval(...)`. It hands a string to the JavaScript engine to parse
|
|
477
665
|
// and run at runtime: the body is opaque to the parser, the type checker and
|
|
478
666
|
// every reader, it can reach and mutate the surrounding scope, and feeding it
|
|
@@ -526,6 +714,35 @@ const sharedRules = {
|
|
|
526
714
|
// guard. The rule is not auto-fixable because only the author knows whether a
|
|
527
715
|
// given `console.log` should be deleted or promoted to a real log channel.
|
|
528
716
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
717
|
+
// Disallow the `debugger` statement. A `debugger` left in source pauses
|
|
718
|
+
// execution under an attached devtools/inspector and is otherwise a no-op, so
|
|
719
|
+
// it is purely a debugging artifact that should never reach a commit — the
|
|
720
|
+
// statement-shaped sibling of the throwaway `console.log` that `no-console`
|
|
721
|
+
// directly above already bans. It is invisible to the type checker and most
|
|
722
|
+
// code review, ships silently, and is exactly the leftover an AI assistant
|
|
723
|
+
// emits while "stepping through" a problem. `eslint-config-agent` does not
|
|
724
|
+
// extend `eslint:recommended` (where this rule lives), so it must be enabled
|
|
725
|
+
// explicitly. The rule is auto-fixable — `eslint --fix` deletes the
|
|
726
|
+
// statement — so adoption is cheap. Several of my repos (e.g.
|
|
727
|
+
// `tupe12334/animals-shop`) re-add `no-debugger: 'error'` by hand on top of
|
|
728
|
+
// the shared config; porting it here removes that copy-paste.
|
|
729
|
+
'no-debugger': 'error',
|
|
730
|
+
// Disallow `alert()`, `confirm()`, and `prompt()`. These browser dialog APIs
|
|
731
|
+
// are exclusively debugging artifacts: `alert(value)` is the browser-native
|
|
732
|
+
// counterpart of `console.log` — a quick "did I reach here / what is this
|
|
733
|
+
// value?" check — but one that blocks the UI thread and cannot be silenced in
|
|
734
|
+
// production without a runtime patch to `window.alert`. Like `no-console` and
|
|
735
|
+
// `no-debugger` directly above, the rule exists to prevent leftover debugging
|
|
736
|
+
// scaffolding from reaching a commit: the three rules together guard every
|
|
737
|
+
// major "quick check" channel that AI assistants reach for when they want to
|
|
738
|
+
// inspect state or get user input inline without wiring a real UI or log
|
|
739
|
+
// channel. `confirm` and `prompt` are included because they carry the same
|
|
740
|
+
// blocking, production-hostile behavior and are equally likely to appear in
|
|
741
|
+
// AI-generated event handlers ("just pop a confirm dialog for now"). The rule
|
|
742
|
+
// is not in `eslint:recommended`, so it must be enabled explicitly. It is not
|
|
743
|
+
// auto-fixable because only the author knows whether the call should be
|
|
744
|
+
// deleted, replaced with a proper log, or wired to a real modal component.
|
|
745
|
+
'no-alert': 'error',
|
|
529
746
|
// Require a regex literal (`/\d+/`) instead of the `RegExp` constructor with
|
|
530
747
|
// a string argument (`new RegExp('\\d+')`, `RegExp('\\d+')`) when the pattern
|
|
531
748
|
// is a static string. The string form forces every backslash to be escaped
|
|
@@ -547,6 +764,78 @@ const sharedRules = {
|
|
|
547
764
|
// runtime, is left untouched. The rule is auto-fixable for the simple cases,
|
|
548
765
|
// so consumers can adopt much of it with `eslint --fix`.
|
|
549
766
|
'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
|
|
767
|
+
// Require named capture groups (`(?<year>\d{4})`) instead of positional ones
|
|
768
|
+
// (`(\d{4})`). An unnamed capture group is referenced by a fragile positional
|
|
769
|
+
// index — `match[1]`, `match[2]` — whose meaning evaporates the moment a
|
|
770
|
+
// group is added, removed, or reordered: `match[1]` silently shifts to a
|
|
771
|
+
// different piece of the string with no type error and no runtime warning.
|
|
772
|
+
// Named groups surface the intent in the regex itself (`(?<year>\d{4})`) and
|
|
773
|
+
// are accessed by key (`match.groups.year`), so the code stays readable and
|
|
774
|
+
// correct even as the pattern evolves. This is the regex-shaped sibling of
|
|
775
|
+
// the explicit-over-clever stance this config already enforces in the rest of
|
|
776
|
+
// the rule set: an unnamed group is exactly the "I'll remember what index 2
|
|
777
|
+
// means" shortcut that doesn't age and that AI assistants emit by default
|
|
778
|
+
// whenever they scaffold a regex. Non-capturing groups (`(?:...)`) are
|
|
779
|
+
// unaffected — only groups that capture and expose a match need a name. The
|
|
780
|
+
// rule is not in `eslint:recommended` and is not covered by
|
|
781
|
+
// `unicorn.configs.all`, so it is enabled explicitly here. It is not
|
|
782
|
+
// auto-fixable because only the author knows what name to give each group.
|
|
783
|
+
'prefer-named-capture-group': 'error',
|
|
784
|
+
// Disallow the comma operator — `a = 1, b = 2`, `for (i = 0, j = 0; ...)`,
|
|
785
|
+
// `return a++, b`. The comma operator evaluates both operands left-to-right
|
|
786
|
+
// and returns the *right-hand* value, silently discarding the left. That is
|
|
787
|
+
// almost never intentional: in almost every real occurrence the author either
|
|
788
|
+
// meant a semicolon (two separate statements), an array literal, or a
|
|
789
|
+
// destructuring assignment — the comma operator just lets the mistake compile
|
|
790
|
+
// without error. It is the hidden-mutation sibling of the assignment
|
|
791
|
+
// discipline rules already here (`no-multi-assign`, `no-return-assign`,
|
|
792
|
+
// `no-useless-assignment`): all of them surface values that look meaningful
|
|
793
|
+
// but are silently discarded or overwritten, exactly the "clever but wrong"
|
|
794
|
+
// pattern this config exists to prevent and one AI assistants emit when
|
|
795
|
+
// packing multiple side effects onto a single expression. The one legitimate
|
|
796
|
+
// use — `for` loop initializers and updates with multiple variables — is
|
|
797
|
+
// carved out by the `allowInParentheses: false` default (the option only
|
|
798
|
+
// affects the parenthesized `(a, b)` form used to suppress some parsers).
|
|
799
|
+
// For `for` loop heads that genuinely need two counters, a destructuring
|
|
800
|
+
// `let [i, j] = [0, 0]` states the intent clearly. The rule is not in
|
|
801
|
+
// `eslint:recommended`, so it is enabled explicitly here. It is not
|
|
802
|
+
// auto-fixable because only the author knows whether to split into separate
|
|
803
|
+
// statements or rewrite the expression entirely.
|
|
804
|
+
'no-sequences': 'error',
|
|
805
|
+
// Disallow empty constructors (`class Foo { constructor() {} }`) and
|
|
806
|
+
// constructors whose only statement forwards all arguments to `super`
|
|
807
|
+
// (`class Foo extends Bar { constructor(...args) { super(...args); } }`). In
|
|
808
|
+
// both cases the constructor adds nothing: JavaScript already synthesizes an
|
|
809
|
+
// implicit empty constructor for base classes and an implicit forwarding
|
|
810
|
+
// constructor for subclasses, so writing them out is pure dead weight — visual
|
|
811
|
+
// noise that looks load-bearing but changes nothing. They are boilerplate an
|
|
812
|
+
// AI assistant reflexively emits when scaffolding a class from a template,
|
|
813
|
+
// putting them squarely in this config's explicit-over-clever, "dead code is
|
|
814
|
+
// not free" stance: the class-declaration siblings of `no-useless-return`,
|
|
815
|
+
// `no-useless-assignment`, and `no-extra-bind` already active here. The rule
|
|
816
|
+
// is auto-fixable, so adopters get a one-shot `eslint --fix`. The core rule
|
|
817
|
+
// is enabled here for JavaScript files; TypeScript files get the same
|
|
818
|
+
// behavior via `@typescript-eslint/no-useless-constructor` enabled by the
|
|
819
|
+
// `strictTypeChecked` preset (which extends `recommended`), so the core rule
|
|
820
|
+
// is turned off in `typescriptEslintRules` to avoid double-reporting.
|
|
821
|
+
'no-useless-constructor': 'error',
|
|
822
|
+
// Disallow returning a value from a class constructor (`return someObject`,
|
|
823
|
+
// `return 5`). `new Thing()` has a contract every caller relies on: the
|
|
824
|
+
// expression evaluates to the instance that was just constructed. Returning
|
|
825
|
+
// an object from the constructor silently overrides that — `new Thing()`
|
|
826
|
+
// hands back a *different* object than the class body initialized, so
|
|
827
|
+
// `instanceof Thing` no longer holds for the value the rest of the program
|
|
828
|
+
// sees. Returning a primitive is the opposite trap: the return value is
|
|
829
|
+
// ignored outright by the `new` semantics, so the statement is dead code
|
|
830
|
+
// that reads as if it does something. Both are exactly the kind of silent,
|
|
831
|
+
// contract-breaking footgun this config exists to catch, and the sort of
|
|
832
|
+
// misplaced `return` an AI assistant emits when it treats a constructor
|
|
833
|
+
// like an ordinary function. A bare early `return;` with no value to bail
|
|
834
|
+
// out of the constructor is unaffected — only returning a value is flagged.
|
|
835
|
+
// The rule is not in `eslint:recommended`, so it is enabled explicitly. It
|
|
836
|
+
// is not auto-fixable because only the author knows whether the returned
|
|
837
|
+
// value or the constructor itself is wrong.
|
|
838
|
+
'no-constructor-return': 'error',
|
|
550
839
|
}
|
|
551
840
|
|
|
552
841
|
// Shared no-restricted-syntax rules for both JS and TS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-config-agent",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "ESLint configuration package with TypeScript support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"configs/",
|
|
41
41
|
"plugins/",
|
|
42
42
|
"exports/",
|
|
43
|
-
"rules
|
|
43
|
+
"rules/**/*.js",
|
|
44
44
|
"!rules/**/examples/",
|
|
45
45
|
"!rules/**/*.spec.js",
|
|
46
46
|
"!rules/**/*.test.js"
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"test:recommended": "node scripts/test-recommended.js",
|
|
69
69
|
"test:recommended-incremental": "node scripts/test-recommended-incremental.js",
|
|
70
70
|
"test:exports": "node scripts/test-exports.js",
|
|
71
|
+
"test:pack": "node scripts/verify-pack.js",
|
|
71
72
|
"test:incremental": "node scripts/test-incremental.js",
|
|
72
73
|
"test:types": "tsc -p test/types/tsconfig.json",
|
|
73
74
|
"test:edge": "eslint test/edge-cases.tsx",
|
package/rules/plugin/n/index.js
CHANGED
|
@@ -2,4 +2,18 @@ import { noProcessEnvironmentConfig } from './no-process-env/index.js'
|
|
|
2
2
|
|
|
3
3
|
export const nRules = {
|
|
4
4
|
'n/no-process-env': noProcessEnvironmentConfig,
|
|
5
|
+
// Require the `node:` protocol prefix on every import of a Node.js built-in
|
|
6
|
+
// module (e.g. `import fs from 'node:fs'` instead of `import fs from 'fs'`).
|
|
7
|
+
// The bare form (`'fs'`, `'path'`, `'crypto'`) is ambiguous: without the
|
|
8
|
+
// protocol, a reader (and a bundler) must consult package.json to confirm
|
|
9
|
+
// that the name is not a userland npm package shadowing the built-in. The
|
|
10
|
+
// `node:` prefix is unambiguous at a glance, is supported since Node 14.18.0
|
|
11
|
+
// (the minimum already required by this package's `engines` field), and is
|
|
12
|
+
// the form the package itself uses in all its own scripts and custom rules
|
|
13
|
+
// (e.g. `import { existsSync } from 'node:fs'`). Shipping the rule in the
|
|
14
|
+
// shared set means consuming projects inherit the same convention
|
|
15
|
+
// automatically. The rule is auto-fixable (`eslint --fix`), so adoption is
|
|
16
|
+
// free. It fires only when a built-in module is actually imported — browser
|
|
17
|
+
// and React code that imports nothing from Node.js is unaffected.
|
|
18
|
+
'n/prefer-node-protocol': 'error',
|
|
5
19
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export const reactRules = {
|
|
2
|
-
|
|
2
|
+
// Require self-closing tags for JSX components and HTML elements that have
|
|
3
|
+
// no children. `<Button></Button>` and `<div></div>` become `<Button />` and
|
|
4
|
+
// `<div />` respectively. An explicit open/close tag pair is meaningful only
|
|
5
|
+
// when it contains children; without children it is pure noise — it adds an
|
|
6
|
+
// extra close tag the reader has to scan past to confirm nothing is inside,
|
|
7
|
+
// and it is exactly the verbose form AI assistants emit when scaffolding
|
|
8
|
+
// components from a template. The single-tag form states "this element has
|
|
9
|
+
// no children" directly, which is the explicit, low-noise style this config
|
|
10
|
+
// favors throughout. The rule is auto-fixable, so `eslint --fix` collapses
|
|
11
|
+
// every violating element in one pass. `component: true` covers React
|
|
12
|
+
// components (`<MyComp></MyComp>`) and `html: true` covers intrinsic
|
|
13
|
+
// elements (`<div></div>`, `<span></span>`), so both surfaces stay
|
|
14
|
+
// consistent. `oss-il` already enforces this manually on top of the base
|
|
15
|
+
// config; promoting it here removes that copy-paste.
|
|
16
|
+
'react/self-closing-comp': ['error', { component: true, html: true }],
|
|
3
17
|
'react/destructuring-assignment': 'off',
|
|
4
18
|
'react/jsx-props-no-spreading': 'off',
|
|
5
19
|
'react/button-has-type': 'off',
|
|
@@ -12,7 +26,16 @@ export const reactRules = {
|
|
|
12
26
|
'react/require-default-props': 'off',
|
|
13
27
|
'react/jsx-wrap-multilines': 'off',
|
|
14
28
|
'react/jsx-closing-bracket-location': 'off',
|
|
15
|
-
|
|
29
|
+
// Forbid fragments that wrap a single child: `<><MyComponent /></>` adds DOM
|
|
30
|
+
// overhead and a return-type mismatch (`React.ReactElement` vs `JSX.Element`)
|
|
31
|
+
// without buying anything. It is exactly the kind of dead-weight an AI
|
|
32
|
+
// assistant emits when scaffolding a component. `allowExpressions: false`
|
|
33
|
+
// also covers `<>{value}</>` — if you need a fragment solely for a JSX
|
|
34
|
+
// expression, wrap it in a parent instead. The rule is auto-fixable, so
|
|
35
|
+
// `eslint --fix` rewrites `<><X /></>` to `<X />` with zero manual work.
|
|
36
|
+
// oss-il already enables this by hand on top of the base config; promoting
|
|
37
|
+
// it here removes the per-repo copy-paste.
|
|
38
|
+
'react/jsx-no-useless-fragment': ['error', { allowExpressions: false }],
|
|
16
39
|
// Guard against React's "leaked render" bug: a short-circuit like
|
|
17
40
|
// `{count && <List />}` renders the literal `0` (or `NaN`) when the left
|
|
18
41
|
// operand is a falsy *non-boolean*, and `{name && <h1>{name}</h1>}` renders
|
|
@@ -62,6 +85,23 @@ export const reactRules = {
|
|
|
62
85
|
// intentional decision visible in code review. The rule is not auto-fixable,
|
|
63
86
|
// so each suppression is a deliberate act.
|
|
64
87
|
'react/no-danger': 'error',
|
|
88
|
+
// Enforce the `[value, setValue]` naming convention for `useState` pairs.
|
|
89
|
+
// When `useState` is destructured with a setter that does not match
|
|
90
|
+
// `set<Capitalize(stateName)>` — e.g. `const [data, handleUpdate] = useState()`
|
|
91
|
+
// instead of `const [data, setData] = useState()` — the mismatch signals a
|
|
92
|
+
// naming mistake that often means the wrong setter was wired up: the right
|
|
93
|
+
// value is read but a misnamed variable is written on update, a class of
|
|
94
|
+
// silent state-management bug that type-checking cannot catch (both names are
|
|
95
|
+
// just `Dispatch<SetStateAction<T>>`). AI assistants introduce exactly this
|
|
96
|
+
// slip when stitching a component from multiple snippets — emitting
|
|
97
|
+
// `const [user, setCurrentUser] = useState()` or
|
|
98
|
+
// `const [loading, handleLoadingChange] = useState(false)`, each of which
|
|
99
|
+
// compiles fine but breaks the per-component naming contract that makes state
|
|
100
|
+
// variables identifiable at a glance. The rule is not auto-fixable because
|
|
101
|
+
// only the author knows whether the state name or the setter name is the
|
|
102
|
+
// intended canonical one. It fires only on `.tsx`/`.jsx` files, so pure
|
|
103
|
+
// TypeScript packages are unaffected.
|
|
104
|
+
'react/hook-use-state': 'error',
|
|
65
105
|
// Forbid a freshly-constructed value as the `value` of a Context Provider:
|
|
66
106
|
// `<Ctx.Provider value={{ user, setUser }}>` or `value={[state, dispatch]}`
|
|
67
107
|
// or `value={() => ...}`. The object/array/function literal is rebuilt on
|
|
@@ -82,4 +122,43 @@ export const reactRules = {
|
|
|
82
122
|
// is why a downstream repo (`oss-il`) already re-adds it by hand on top of the
|
|
83
123
|
// base config.
|
|
84
124
|
'react/jsx-no-constructed-context-values': 'error',
|
|
125
|
+
// Forbid using an object type (`{}`, `[]`, or a function literal) as the
|
|
126
|
+
// default value for a prop in function-component destructuring:
|
|
127
|
+
//
|
|
128
|
+
// function List({ items = [], onSelect = () => {} }) { ... }
|
|
129
|
+
//
|
|
130
|
+
// Each call to the component creates a fresh object/array/function literal
|
|
131
|
+
// for the default, so the reference changes on every render even when the
|
|
132
|
+
// prop is not actually provided. Any child that is `React.memo`-ed on that
|
|
133
|
+
// prop, or any `useEffect`/`useCallback`/`useMemo` that lists it as a
|
|
134
|
+
// dependency, will re-run unnecessarily — a silent performance bug the type
|
|
135
|
+
// checker cannot detect. The fix is to hoist the constant outside the
|
|
136
|
+
// component or memoize it:
|
|
137
|
+
//
|
|
138
|
+
// const DEFAULT_ITEMS: string[] = []
|
|
139
|
+
// function List({ items = DEFAULT_ITEMS }) { ... }
|
|
140
|
+
//
|
|
141
|
+
// This is exactly the shortcut an AI assistant reaches for when a prop is
|
|
142
|
+
// optional and the model doesn't want to add a module-level constant. The
|
|
143
|
+
// rule sits on the same correctness/perf-not-style bar as `jsx-no-leaked-
|
|
144
|
+
// render`, `no-array-index-key`, and `jsx-no-constructed-context-values`
|
|
145
|
+
// above — all of which catch silent reference-equality bugs that are
|
|
146
|
+
// invisible to the type checker. Like the other react rules this only fires
|
|
147
|
+
// on `.jsx`/`.tsx` files, so non-React TypeScript packages are unaffected.
|
|
148
|
+
'react/no-object-type-as-default-prop': 'error',
|
|
149
|
+
// Require a `key` prop on every element created inside an array/iterator
|
|
150
|
+
// (`.map()`, a spread of JSX literals, etc.). `key` is React's per-child
|
|
151
|
+
// identity across renders; without it React falls back to matching children
|
|
152
|
+
// by position, so as soon as the list is reordered, filtered, or gains/loses
|
|
153
|
+
// an item, state and DOM nodes "smear" onto the wrong element — text typed
|
|
154
|
+
// into one row's input jumps to another, the wrong checkbox stays checked,
|
|
155
|
+
// an animation plays on the wrong item. The JSX is perfectly typed either
|
|
156
|
+
// way, so this is a real rendering bug invisible to the type checker, and
|
|
157
|
+
// exactly the shortcut an AI assistant reaches for when it renders a list
|
|
158
|
+
// without pausing to think about identity. It sits on the same
|
|
159
|
+
// correctness-not-style bar as `no-array-index-key`, `jsx-no-leaked-render`,
|
|
160
|
+
// and `jsx-no-constructed-context-values` above. Like the other react rules
|
|
161
|
+
// it only applies to `.jsx`/`.tsx` files, so non-React TypeScript packages
|
|
162
|
+
// are unaffected.
|
|
163
|
+
'react/jsx-key': 'error',
|
|
85
164
|
}
|
|
@@ -2,8 +2,58 @@ import { noExplicitAnyConfig } from './no-explicit-any/index.js'
|
|
|
2
2
|
|
|
3
3
|
export const typescriptEslintRules = {
|
|
4
4
|
...noExplicitAnyConfig,
|
|
5
|
+
// `strictTypeChecked` (which this config extends) enables
|
|
6
|
+
// `@typescript-eslint/only-throw-error` — the type-aware replacement for the
|
|
7
|
+
// core `no-throw-literal` rule that catches additional cases (e.g. throwing a
|
|
8
|
+
// variable typed as `string | Error`). The core rule is still active in
|
|
9
|
+
// `sharedRules` and, because `sharedRules` is spread into the TypeScript
|
|
10
|
+
// config *after* the preset, it overrides the preset's own `no-throw-literal:
|
|
11
|
+
// 'off'`. Turning the core rule off here (in `typescriptEslintRules`, which
|
|
12
|
+
// is spread after `sharedRules`) ensures TypeScript files are checked only by
|
|
13
|
+
// the type-aware rule so there is no double-reporting of the same violation.
|
|
14
|
+
// The core rule remains active for `.js`/`.jsx` files via `sharedRules`,
|
|
15
|
+
// where the type-aware version does not run.
|
|
16
|
+
'no-throw-literal': 'off',
|
|
17
|
+
// `strictTypeChecked` (which this config extends) includes `recommended`,
|
|
18
|
+
// which enables `@typescript-eslint/no-useless-constructor` for TypeScript
|
|
19
|
+
// files. The core `no-useless-constructor` rule is active in `sharedRules`
|
|
20
|
+
// for JavaScript files. Turning the core rule off here ensures TypeScript
|
|
21
|
+
// files are covered only by the TypeScript-aware variant, preventing
|
|
22
|
+
// double-reporting on the same constructor.
|
|
23
|
+
'no-useless-constructor': 'off',
|
|
24
|
+
// `stylisticTypeChecked` (which this config extends) enables
|
|
25
|
+
// `@typescript-eslint/prefer-nullish-coalescing`, recommending `??` over `||`
|
|
26
|
+
// for nullable-type checks. However, this config deliberately bans the `??`
|
|
27
|
+
// operator via a `no-restricted-syntax` rule (see `noNullishCoalescingConfig`)
|
|
28
|
+
// in favour of explicit `!== null && !== undefined` checks. With both active,
|
|
29
|
+
// a user writing `x || 'default'` (where `x` is nullable) gets
|
|
30
|
+
// `prefer-nullish-coalescing` telling them to use `??`, and then
|
|
31
|
+
// `no-restricted-syntax` forbidding the `??` they just added — an
|
|
32
|
+
// unresolvable contradiction. Turning this rule off here lets
|
|
33
|
+
// `no-restricted-syntax` be the single, authoritative constraint: reach for
|
|
34
|
+
// explicit null checks, not `??`.
|
|
35
|
+
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
|
5
36
|
'@typescript-eslint/consistent-type-assertions': 'off',
|
|
6
37
|
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
|
38
|
+
// Force the **property style** (`foo: (x: number) => void`) for every method
|
|
39
|
+
// member of an interface or object type, and forbid the **method shorthand**
|
|
40
|
+
// (`foo(x: number): void`). The two forms look interchangeable but are not:
|
|
41
|
+
// TypeScript deliberately exempts method-shorthand signatures from
|
|
42
|
+
// `strictFunctionTypes` and checks them *bivariantly* (a documented language
|
|
43
|
+
// unsoundness), while property-style signatures are checked *contravariantly*
|
|
44
|
+
// (sound). In practice a method-shorthand declaration silently accepts an
|
|
45
|
+
// incompatible callback or override that the property style would reject —
|
|
46
|
+
// the incompatibility passes type-checking and only surfaces as a wrong call
|
|
47
|
+
// at runtime. That is exactly the "looks typed, fails at runtime" gap this
|
|
48
|
+
// config exists to close, and it is the signature-side companion of the
|
|
49
|
+
// already-enabled `no-non-null-assertion` and `require-array-sort-compare`
|
|
50
|
+
// rules. The rule is *not* in typescript-eslint's `strictTypeChecked` preset
|
|
51
|
+
// this config extends, so it must be turned on explicitly. It is
|
|
52
|
+
// **auto-fixable** (`eslint --fix`), so adoption costs nothing. Both
|
|
53
|
+
// `tupe12334/zod-utils` and `tupe12334/tools-view` already re-add it by hand
|
|
54
|
+
// on top of the shared config; promoting it here removes that copy-paste and
|
|
55
|
+
// covers every downstream consumer.
|
|
56
|
+
'@typescript-eslint/method-signature-style': ['error', 'property'],
|
|
7
57
|
// Require a `case` for every member of the union/enum a `switch` discriminates
|
|
8
58
|
// on. This is the type-aware completion of the `no-restricted-syntax` ban on
|
|
9
59
|
// `default` cases this config already ships ("Default cases are not allowed in
|
|
@@ -139,6 +189,37 @@ export const typescriptEslintRules = {
|
|
|
139
189
|
// by hand on top of the base config — promoting it into the shared rule set
|
|
140
190
|
// removes that copy-paste.
|
|
141
191
|
'@typescript-eslint/no-shadow': 'error',
|
|
192
|
+
// Forbid referencing a `let`/`const`/`class` binding before its textual
|
|
193
|
+
// declaration. Thanks to the Temporal Dead Zone, an early reference to such
|
|
194
|
+
// a binding does not read `undefined` — it throws a `ReferenceError` at
|
|
195
|
+
// runtime, so the file can type-check and look correct while a particular
|
|
196
|
+
// code path (a closure invoked before its outer scope finishes
|
|
197
|
+
// initializing, a module-level `const` referenced by another before both
|
|
198
|
+
// have run) blows up only when actually executed. That looks-right-fails-
|
|
199
|
+
// at-runtime gap is exactly what this config exists to close. Declaration
|
|
200
|
+
// order should be a reliable signal of evaluation order; this rule makes
|
|
201
|
+
// violations a lint error instead of a surprise crash.
|
|
202
|
+
//
|
|
203
|
+
// The core `no-use-before-define` rule is intentionally left `off` (see
|
|
204
|
+
// `sharedRules` in `index.js`): it does not understand TypeScript's hoisted
|
|
205
|
+
// type-level declarations (`interface`, `type`, and `enum` members are safe
|
|
206
|
+
// to reference before their textual position) and would false-positive on
|
|
207
|
+
// them. The typescript-eslint version understands those cases, so it is the
|
|
208
|
+
// documented replacement rather than a second rule fighting the first. It
|
|
209
|
+
// needs no type information, so it adds no parser cost.
|
|
210
|
+
//
|
|
211
|
+
// `functions: false` exempts function declarations, which are fully
|
|
212
|
+
// hoisted (including their body) and safe to call before their textual
|
|
213
|
+
// definition — flagging them would only add noise, not catch a real bug.
|
|
214
|
+
// `classes` and `variables` stay `true` because those bindings are hoisted
|
|
215
|
+
// but left uninitialized (the TDZ), so referencing them early is the actual
|
|
216
|
+
// runtime hazard this rule exists to catch. This mirrors how downstream repo
|
|
217
|
+
// `ameliso-io/web` already configures the rule by hand on top of the base
|
|
218
|
+
// config — promoting it into the shared rule set removes that copy-paste.
|
|
219
|
+
'@typescript-eslint/no-use-before-define': [
|
|
220
|
+
'error',
|
|
221
|
+
{ functions: false, classes: true, variables: true },
|
|
222
|
+
],
|
|
142
223
|
// Forbid declaring a function inside a loop when that function closes over a
|
|
143
224
|
// binding that changes between iterations. A function created in a loop
|
|
144
225
|
// captures its outer variables *by reference*, not by value, so every closure
|
|
@@ -162,6 +243,19 @@ export const typescriptEslintRules = {
|
|
|
162
243
|
// signal without the false positives the core rule raises in typed code. It
|
|
163
244
|
// needs no type information, so it adds no parser cost.
|
|
164
245
|
'@typescript-eslint/no-loop-func': 'error',
|
|
246
|
+
// Prefer optional chaining (`a?.b?.c`) over a chain of `&&` null-guards
|
|
247
|
+
// (`a && a.b && a.b.c`). The `&&`-chain form evaluates the base expression
|
|
248
|
+
// repeatedly and handles `0`, `""`, and `false` as "missing" — a falsy-value
|
|
249
|
+
// coercion bug the type checker cannot catch because the operator is
|
|
250
|
+
// perfectly typed. Optional chaining short-circuits on `null`/`undefined`
|
|
251
|
+
// only, evaluates the base once, and is the form TypeScript was designed to
|
|
252
|
+
// express. It is also shorter and reads left-to-right with no repeated
|
|
253
|
+
// sub-expressions. The rule is auto-fixable (`eslint --fix`), so adoption is
|
|
254
|
+
// cheap. It lives in typescript-eslint's `stylistic-type-checked` preset but
|
|
255
|
+
// NOT in `strict-type-checked`, which is why this config (extending
|
|
256
|
+
// `strictTypeChecked`) must enable it explicitly — and why a downstream repo
|
|
257
|
+
// (`tools-view`) already re-adds it by hand on top of the base config.
|
|
258
|
+
'@typescript-eslint/prefer-optional-chain': 'error',
|
|
165
259
|
// Require any function that returns a `Promise` to be declared `async`. A
|
|
166
260
|
// plain (non-`async`) function that returns a promise can still throw
|
|
167
261
|
// *synchronously* — anything that runs before the promise is constructed (an
|
|
@@ -208,4 +302,112 @@ export const typescriptEslintRules = {
|
|
|
208
302
|
// running under the `projectService` parser options already configured for
|
|
209
303
|
// `.ts`/`.tsx`.
|
|
210
304
|
'@typescript-eslint/return-await': ['error', 'in-try-catch'],
|
|
305
|
+
// Flag Promises used in positions where their resolution value cannot be
|
|
306
|
+
// observed — the fourth side of the async-hygiene quadrant already in this
|
|
307
|
+
// config (`no-floating-promises`, `promise-function-async`, `return-await`).
|
|
308
|
+
//
|
|
309
|
+
// Three patterns are caught:
|
|
310
|
+
//
|
|
311
|
+
// 1. `checksVoidReturn` (default `true`): an async function passed where the
|
|
312
|
+
// callback's return type is `void` — the textbook case is
|
|
313
|
+
// `array.forEach(async item => { await work(item) })`. The returned
|
|
314
|
+
// `Promise<void>` is structurally assignable to `void`, so TypeScript's
|
|
315
|
+
// own type-checker never flags it, but the Promise is silently dropped and
|
|
316
|
+
// its rejection becomes an unhandled rejection crash. The fix is an
|
|
317
|
+
// explicit `for...of` loop with `await`, or an `async` wrapper that is
|
|
318
|
+
// then awaited by the caller.
|
|
319
|
+
//
|
|
320
|
+
// 2. `checksConditionals` (default `true`): a Promise used directly in a
|
|
321
|
+
// boolean context — `if (fetchData())` or `while (poll())`. The Promise
|
|
322
|
+
// object itself is always truthy, so the condition never reflects the
|
|
323
|
+
// resolved value; this is almost always a missing `await`.
|
|
324
|
+
//
|
|
325
|
+
// 3. `checksSpreads` (default `true`): `{...asyncFn()}` spreads the Promise
|
|
326
|
+
// object's own properties (none meaningful) instead of the resolved
|
|
327
|
+
// object's properties — a silent data-loss bug.
|
|
328
|
+
//
|
|
329
|
+
// All three are silent: TypeScript does not flag them, the code runs without
|
|
330
|
+
// a syntax error, and the bug surfaces at runtime as either a missing
|
|
331
|
+
// side-effect, an always-true conditional, or an empty object spread. They
|
|
332
|
+
// are exactly the "looks correct, fails at runtime" shortcuts an AI assistant
|
|
333
|
+
// produces when it wraps async work in a sync-looking callback or forgets an
|
|
334
|
+
// `await`. The rule is type-aware, so it runs under the `projectService`
|
|
335
|
+
// parser options already configured for `.ts`/`.tsx` files. Downstream repos
|
|
336
|
+
// (`book-processor`) already promote this rule to `error` on top of the base
|
|
337
|
+
// config — promoting it into the shared rule set removes that copy-paste.
|
|
338
|
+
'@typescript-eslint/no-misused-promises': 'error',
|
|
339
|
+
// Require an explicit initializer for every enum member. Without one,
|
|
340
|
+
// TypeScript assigns implicit numeric values (0, 1, 2, …) based on the
|
|
341
|
+
// member's position in the declaration. Reordering members or inserting a
|
|
342
|
+
// new one in the middle silently changes every subsequent member's numeric
|
|
343
|
+
// value — a breaking API change for any caller that stored or compared the
|
|
344
|
+
// raw numbers. The type-checker cannot flag this: the values are still typed
|
|
345
|
+
// as the enum, not as literal `0`/`1`/`2`, so the mismatch only surfaces at
|
|
346
|
+
// runtime. Writing an explicit initializer (`= 'Up'`, `= 1`) makes the
|
|
347
|
+
// assigned value visible and independent of position: adding or reordering
|
|
348
|
+
// members is now safe by construction, and the intent is legible without
|
|
349
|
+
// counting indices. It is exactly the "looks stable, breaks on extension"
|
|
350
|
+
// footgun AI assistants introduce when they generate an enum and leave the
|
|
351
|
+
// compiler to decide the values — the enum counterpart of the
|
|
352
|
+
// `array-callback-return` and `switch-exhaustiveness-check` rules this
|
|
353
|
+
// config already ships for other "silent missing value" bugs. The rule is
|
|
354
|
+
// not in `strictTypeChecked` or `stylisticTypeChecked`, so it must be
|
|
355
|
+
// enabled explicitly. It is not auto-fixable: only the author knows what
|
|
356
|
+
// each member's value should be.
|
|
357
|
+
'@typescript-eslint/prefer-enum-initializers': 'error',
|
|
358
|
+
// Forbid enum declarations that mix numeric and string member values. TypeScript
|
|
359
|
+
// allows both `Status.Active = 0` (number) and `Status.Name = 'active'`
|
|
360
|
+
// (string) in the same declaration, but the resulting type widens in
|
|
361
|
+
// surprising ways: a mixed enum has no reverse-mapping for string members,
|
|
362
|
+
// exhaustiveness checks over it behave differently depending on TypeScript
|
|
363
|
+
// version, and `Object.values(Status)` returns `[0, 'active']` — a
|
|
364
|
+
// heterogeneous array the caller rarely expects. Combined with the existing
|
|
365
|
+
// `prefer-enum-initializers` rule (which makes every member's value
|
|
366
|
+
// explicit), this rule makes the *kind* of value explicit too: all numbers or
|
|
367
|
+
// all strings, never both. It is exactly the implicit ambiguity an AI
|
|
368
|
+
// assistant introduces when it scaffolds an enum from a mixed-type spec ("id
|
|
369
|
+
// is 0, label is 'active'") without deciding which kind to use. The rule is
|
|
370
|
+
// not in `strictTypeChecked` or `stylisticTypeChecked`, so it must be enabled
|
|
371
|
+
// explicitly. It is not auto-fixable: only the author knows whether to
|
|
372
|
+
// normalize to numbers or strings.
|
|
373
|
+
'@typescript-eslint/no-mixed-enums': 'error',
|
|
374
|
+
// Flag a condition, `&&`/`||` operand, or optional-chain (`?.`) link whose
|
|
375
|
+
// type proves it can never be anything but truthy or never be anything but
|
|
376
|
+
// falsy. A guard like `if (value)` where `value`'s type is a non-nullable
|
|
377
|
+
// object looks like a real null/undefined check, but the type checker has
|
|
378
|
+
// already proven it always passes — the "guard" is dead code that silently
|
|
379
|
+
// hides the fact the author expected a case (`null`, `undefined`, `false`)
|
|
380
|
+
// the type no longer allows, usually because an earlier refactor narrowed
|
|
381
|
+
// the type and left a now-redundant check behind. The inverse (a condition
|
|
382
|
+
// that is always falsy) is worse: the guarded branch is unreachable dead
|
|
383
|
+
// code that looks live. Both are exactly the "looks like a safety check,
|
|
384
|
+
// does nothing" gap this config exists to close, and the type-aware
|
|
385
|
+
// completion of the same always-true/false class of bug
|
|
386
|
+
// `no-self-compare` and `no-constant-binary-expression` already catch by
|
|
387
|
+
// syntax alone. The rule is type-aware, so it runs under the
|
|
388
|
+
// `projectService` parser options already configured for `.ts`/`.tsx`
|
|
389
|
+
// files, and it is why downstream repo `block-no-verify` already re-adds
|
|
390
|
+
// it by hand on top of the base config.
|
|
391
|
+
'@typescript-eslint/no-unnecessary-condition': 'error',
|
|
392
|
+
// Forbid a `private` (TypeScript keyword) or `#hashPrivate` class field or
|
|
393
|
+
// method that is declared and never read or called anywhere in the class
|
|
394
|
+
// body. Because the member is private, there is no legitimate external
|
|
395
|
+
// caller to account for — an unused one is dead code: a leftover from a
|
|
396
|
+
// refactor that forgot to delete it, or worse, a member the author meant to
|
|
397
|
+
// wire up but a typo or missed call site left orphaned, silently doing
|
|
398
|
+
// nothing while the reader assumes it does something.
|
|
399
|
+
//
|
|
400
|
+
// The core `no-unused-private-class-members` rule (already enabled via
|
|
401
|
+
// `eslint:recommended`) only understands ECMAScript `#hashPrivate` fields —
|
|
402
|
+
// it has no notion of TypeScript's `private` keyword, which is the actual
|
|
403
|
+
// dead-code surface in a TypeScript-heavy codebase. The typescript-eslint
|
|
404
|
+
// version is a strict superset (flags both forms) and needs no type
|
|
405
|
+
// information, so it adds no parser cost. This mirrors how
|
|
406
|
+
// `@typescript-eslint/no-shadow` and `@typescript-eslint/no-use-before-define`
|
|
407
|
+
// above already replace their core counterparts. Downstream repo
|
|
408
|
+
// `ameliso-io/web` already re-adds both the core and typescript-eslint
|
|
409
|
+
// variants by hand on top of the base config — promoting the
|
|
410
|
+
// typescript-eslint version into the shared rule set removes that
|
|
411
|
+
// copy-paste.
|
|
412
|
+
'@typescript-eslint/no-unused-private-class-members': 'error',
|
|
211
413
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* eslint-disable single-export/single-export */
|
|
2
|
+
// Helpers for the require-spec-file-tsx rule.
|
|
3
|
+
//
|
|
4
|
+
// Logic detection and pattern matching mirror eslint-plugin-ddd so the
|
|
5
|
+
// .tsx/.jsx half of the spec-file requirement behaves like the .js/.ts half.
|
|
6
|
+
|
|
7
|
+
export const JSX_EXTENSIONS = ['.tsx', '.jsx']
|
|
8
|
+
|
|
9
|
+
const FUNC_TYPES = new Set([
|
|
10
|
+
'FunctionDeclaration',
|
|
11
|
+
'FunctionExpression',
|
|
12
|
+
'ArrowFunctionExpression',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
// Arrow bodies that are a bare value (not a block) and carry no real logic.
|
|
16
|
+
const SIMPLE_BODIES = new Set([
|
|
17
|
+
'Literal',
|
|
18
|
+
'Identifier',
|
|
19
|
+
'ObjectExpression',
|
|
20
|
+
'ArrayExpression',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
// Whether a node introduces real logic that warrants a spec file.
|
|
24
|
+
export const hasLogicInNode = node => {
|
|
25
|
+
if (!node) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
if (FUNC_TYPES.has(node.type) && node.body) {
|
|
29
|
+
if (
|
|
30
|
+
node.type === 'ArrowFunctionExpression' &&
|
|
31
|
+
node.body.type !== 'BlockStatement'
|
|
32
|
+
) {
|
|
33
|
+
return !SIMPLE_BODIES.has(node.body.type)
|
|
34
|
+
}
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
38
|
+
return node.body.body.some(
|
|
39
|
+
member =>
|
|
40
|
+
member.type === 'MethodDefinition' && member.value && member.value.body
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checkSinglePattern = (filename, pattern) => {
|
|
47
|
+
if (pattern.startsWith('**/') && pattern.endsWith('/**')) {
|
|
48
|
+
return filename.includes(`/${pattern.slice(3, -3)}/`)
|
|
49
|
+
}
|
|
50
|
+
if (pattern.includes('**/')) {
|
|
51
|
+
const suffix = pattern.replace('**/', '')
|
|
52
|
+
if (suffix.startsWith('*')) {
|
|
53
|
+
return filename.endsWith(suffix.slice(1))
|
|
54
|
+
}
|
|
55
|
+
if (suffix.includes('*')) {
|
|
56
|
+
const basename = filename.split('/').pop()
|
|
57
|
+
return suffix.split('*').every(part => basename.includes(part))
|
|
58
|
+
}
|
|
59
|
+
return filename.endsWith(`/${suffix}`)
|
|
60
|
+
}
|
|
61
|
+
return filename.includes(pattern)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Test a filename against glob-ish exclude patterns, supporting the same
|
|
65
|
+
// double-star prefix and brace-expansion forms accepted by the ddd plugin.
|
|
66
|
+
export const checkExcludePatterns = (filename, excludePatterns) => {
|
|
67
|
+
const normalized = filename.replaceAll('\\', '/')
|
|
68
|
+
return excludePatterns.some(pattern => {
|
|
69
|
+
const braceMatch = pattern.match(/\{(?<content>[^}]+)\}/)
|
|
70
|
+
if (braceMatch) {
|
|
71
|
+
return braceMatch.groups.content
|
|
72
|
+
.split(',')
|
|
73
|
+
.some(alt =>
|
|
74
|
+
checkSinglePattern(normalized, pattern.replace(/\{[^}]+\}/, alt))
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return checkSinglePattern(normalized, pattern)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const DEFAULT_EXCLUDE_PATTERNS = [
|
|
82
|
+
'**/index.tsx',
|
|
83
|
+
'**/index.jsx',
|
|
84
|
+
'**/*.stories.{tsx,jsx}',
|
|
85
|
+
'**/*.d.ts',
|
|
86
|
+
// Error/exception files mirror the ddd/require-spec-file exemptions so a
|
|
87
|
+
// React/Preact error component (e.g. an error boundary) is treated the same
|
|
88
|
+
// as its `.ts` counterpart instead of being singled out for a spec file.
|
|
89
|
+
'**/*-error.{tsx,jsx}',
|
|
90
|
+
'**/*.error.{tsx,jsx}',
|
|
91
|
+
'**/errors/**',
|
|
92
|
+
'**/exceptions/**',
|
|
93
|
+
]
|