eslint-config-agent 3.0.4 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +592 -26
  3. package/configs/base-plugins.js +32 -0
  4. package/configs/config-files.js +19 -3
  5. package/configs/examples.js +12 -1
  6. package/configs/javascript.js +12 -4
  7. package/configs/length-rule-scope.js +27 -0
  8. package/configs/overrides.js +3 -14
  9. package/configs/test-files.js +40 -1
  10. package/configs/tsx.js +10 -0
  11. package/configs/typescript.js +44 -2
  12. package/exports/ddd.d.ts +10 -0
  13. package/exports/ddd.js +1 -3
  14. package/exports/incremental.d.ts +11 -0
  15. package/exports/incremental.js +39 -0
  16. package/exports/recommended-incremental.d.ts +11 -0
  17. package/exports/recommended-incremental.js +46 -0
  18. package/exports/recommended.d.ts +14 -0
  19. package/exports/recommended.js +110 -0
  20. package/exports/to-warnings.d.ts +20 -0
  21. package/exports/to-warnings.js +41 -0
  22. package/index.d.ts +18 -0
  23. package/index.js +883 -7
  24. package/package.json +46 -17
  25. package/plugins/index.js +6 -0
  26. package/rules/index.js +20 -22
  27. package/rules/no-inline-union-types/index.js +8 -6
  28. package/rules/no-process-env-properties/index.js +4 -4
  29. package/rules/no-record-literal-types/index.js +3 -3
  30. package/rules/plugin/import/index.js +41 -0
  31. package/rules/plugin/index.js +0 -2
  32. package/rules/plugin/n/index.js +16 -2
  33. package/rules/plugin/n/no-process-env/index.js +4 -4
  34. package/rules/plugin/react/index.js +150 -2
  35. package/rules/plugin/typescript-eslint/index.js +406 -0
  36. package/rules/require-spec-file-tsx/README.md +57 -0
  37. package/rules/require-spec-file-tsx/helpers.js +93 -0
  38. package/rules/require-spec-file-tsx/index.js +109 -0
  39. package/rules/plugin/jsx-a11y/index.js +0 -3
package/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import js from '@eslint/js'
2
+ import unicorn from 'eslint-plugin-unicorn'
2
3
  import earlyReturn from 'eslint-plugin-early-return'
3
4
  import switchCase from 'eslint-plugin-switch-case'
4
5
  import jsxClassname from 'eslint-plugin-jsx-classname'
5
6
  import jsdoc from 'eslint-plugin-jsdoc'
6
- import reactHooks from 'eslint-plugin-react-hooks'
7
7
  import { defineConfig } from 'eslint/config'
8
8
  import tseslint from 'typescript-eslint'
9
9
  import { basePluginsConfig } from './configs/base-plugins.js'
@@ -26,11 +26,30 @@ 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',
32
45
  'no-unused-vars': 'off',
33
46
  '@typescript-eslint/no-unused-vars': 'off',
47
+ // The core/`@typescript-eslint` `no-unused-vars` rules are disabled above, so
48
+ // nothing flagged dead imports. `eslint-plugin-unused-imports` fills that gap
49
+ // and, unlike the base rules, auto-fixes them: an unused `import` is pure dead
50
+ // weight (no runtime effect, just noise and slower builds) and is always safe
51
+ // to delete. Scoped to imports only — unused locals/args are left alone here.
52
+ 'unused-imports/no-unused-imports': 'error',
34
53
  'max-lines-per-function': allRules.maxFunctionLinesWarning,
35
54
  'max-lines': allRules.maxFileLinesWarning,
36
55
  semi: 'off',
@@ -42,6 +61,781 @@ const sharedRules = {
42
61
  'no-continue': 'off',
43
62
  // Additional built-in error handling rules
44
63
  'prefer-promise-reject-errors': 'error',
64
+ // Disallow returning a value from a `Promise` executor — the function passed
65
+ // to `new Promise((resolve, reject) => { ... })`. The executor's return value
66
+ // is discarded by the Promise constructor, so `new Promise(() => readfile())`
67
+ // or `new Promise((resolve) => resolve(x))` written with an implicit return
68
+ // body silently throws the value away. When the returned value is itself a
69
+ // promise (an async executor, a `.then(...)` chain) this is a real bug: the
70
+ // work runs unobserved, rejections become unhandled, and the outer promise
71
+ // settles on a different schedule than the author expects. This is a
72
+ // correctness check, not a style preference — the same class of "the callback
73
+ // returns into a void" mistake that `array-callback-return` above guards
74
+ // against, and exactly the kind of quiet, wrong-behavior bug that type
75
+ // checking will not catch and that AI assistants emit when they reach for a
76
+ // brace body inside `new Promise`. It is not in `eslint:recommended`, so it is
77
+ // enabled explicitly here. The rule is not auto-fixable because only the
78
+ // author knows whether the value should be dropped or the surrounding control
79
+ // flow reworked.
80
+ 'no-promise-executor-return': 'error',
81
+ // Disallow `await` inside a loop body. An `await` in a `for`/`for-of`/`while`
82
+ // loop pauses the loop on every iteration and only starts the next one after
83
+ // the current promise settles, so N independent async operations that could
84
+ // have run concurrently are instead serialized end-to-end: the loop takes the
85
+ // *sum* of the latencies instead of the *max*. On a list of network/DB calls
86
+ // that turns a sub-second batch into an N-times-slower stall — a quiet
87
+ // performance bug the type checker cannot see (the code is perfectly typed and
88
+ // correct, just slow) and exactly the shape an AI assistant emits when it
89
+ // mechanically drops an `await` into a `for` loop instead of reaching for
90
+ // `Promise.all`/`Promise.allSettled` over a `.map`. This is the throughput
91
+ // side of the async-hygiene family this config already builds with
92
+ // `no-floating-promises`, `promise-function-async` and `return-await`. When
93
+ // the iterations are genuinely dependent (each one needs the previous result,
94
+ // an ordered write, a deliberate rate limit), the serial `await` is correct —
95
+ // the rule has no auto-fix precisely because only the author knows that, so
96
+ // those loops opt out with an inline `// eslint-disable-next-line
97
+ // no-await-in-loop`. It is not in `eslint:recommended`, so it is enabled
98
+ // explicitly here.
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',
114
+ // Disallow an `else`/`else if` block when the preceding `if` already exits
115
+ // the function via `return`. The `else` is dead weight: once the `if` branch
116
+ // returns, the code after it is unreachable from that path, so wrapping the
117
+ // remainder in `else` only adds a level of nesting that hides the real
118
+ // control flow. This is the built-in companion to the `early-return` plugin
119
+ // already enabled here — both push code toward flat, guard-clause style
120
+ // instead of the deeply-nested if/else trees AI assistants tend to emit. The
121
+ // rule is auto-fixable, so consumers can adopt it with `eslint --fix`.
122
+ 'no-else-return': ['error', { allowElseIf: false }],
123
+ // Disallow an `if` statement as the *only* statement inside an `else` block,
124
+ // requiring `else if` instead. The lone `if`-in-`else` adds an indentation
125
+ // level that hides what is really a flat chain of conditions, the same
126
+ // needless nesting the `no-else-return` rule directly above and the bundled
127
+ // `early-return` plugin already push back on, and a shape AI assistants emit
128
+ // when they mechanically wrap each new branch instead of extending the chain.
129
+ // Collapsing it into an `else if` keeps the control flow flat and legible. The
130
+ // rule is auto-fixable, so consumers can adopt it with `eslint --fix`.
131
+ // Note: `unicorn/no-lonely-if` covers a different pattern (lone `if` inside
132
+ // another `if` without `else`), so both rules are active — they are complementary.
133
+ 'no-lonely-if': 'error',
134
+ // Disallow a `return;` (or `return undefined;`) that is the last statement of
135
+ // a function and so changes nothing — control already falls off the end and
136
+ // yields `undefined`. A redundant trailing `return` reads as if it guards
137
+ // something or hands back a meaningful value when it does neither; it is dead
138
+ // punctuation that hides the real control flow, exactly what this config's
139
+ // explicit-over-clever stance exists to surface. It is the natural companion
140
+ // to the `no-else-return` rule just above and to the `early-return` plugin
141
+ // already enabled here: that pair flattens branching by removing unneeded
142
+ // `else` blocks, and this removes the now-pointless `return` they often leave
143
+ // behind. The rule is auto-fixable, so consumers can adopt it with
144
+ // `eslint --fix`.
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',
162
+ // Disallow a value assigned to a variable that is never read before the
163
+ // variable is overwritten or its scope ends — a "dead store". Writing
164
+ // `let total = compute()` and then unconditionally reassigning `total = 0`
165
+ // (or returning before `total` is ever used) throws the first value away,
166
+ // and that wasted write is almost never deliberate: it usually means the
167
+ // author meant to *use* the value, assigned to the wrong variable, or left a
168
+ // half-finished branch behind. The result is code that reads as if a value
169
+ // flows through when it silently does not — exactly the quiet, wrong-behavior
170
+ // bug type checking will not catch (the types line up; only the data flow is
171
+ // broken) and one AI assistants emit when they stitch branches together or
172
+ // forget to thread a computed value into its use. It is the data-flow sibling
173
+ // of the `no-useless-return` rule just above: both surface statements that
174
+ // look load-bearing but change nothing. The rule relies on ESLint's code-path
175
+ // analysis (added in ESLint 9.14) and is not in `eslint:recommended`, so it
176
+ // is enabled explicitly here. It is not auto-fixable because only the author
177
+ // knows whether the dead store should be deleted or its value actually used.
178
+ 'no-useless-assignment': 'error',
179
+ // Disallow assignment inside a `return` statement — `return total = compute()`
180
+ // or `return (found = next)`. An `=` in the spot a reader expects a value (and
181
+ // where `==`/`===` belongs) does two things at once: it mutates a binding and
182
+ // hands back the assigned value, so the side effect hides in the returned
183
+ // expression where almost no one looks for it. It is most often a typo for a
184
+ // comparison (`return found === next`) or a leftover from refactoring a guard,
185
+ // and the code still type-checks because the assigned value has the right type
186
+ // — only the data flow is wrong, exactly the quiet, plausible-but-wrong shape
187
+ // this config exists to surface and one AI assistants emit when they fold an
188
+ // update into a `return`. It sits with the assignment-discipline rules already
189
+ // here — `no-param-reassign`, `no-multi-assign` and the `no-useless-assignment`
190
+ // dead-store check directly above — all of which keep where and how values are
191
+ // written explicit. `'always'` flags the assignment even when wrapped in
192
+ // parentheses, so the deliberate-looking `return (x = y)` form is rejected too;
193
+ // an author who truly means it can split the assignment onto its own line. It
194
+ // is not in `eslint:recommended`, so it is enabled explicitly here, and it is
195
+ // not auto-fixable because only the author knows whether a comparison or a
196
+ // separate statement was intended.
197
+ 'no-return-assign': ['error', 'always'],
198
+ // Disallow ternaries whose branches are themselves a boolean literal
199
+ // (`cond ? true : false`, `cond ? false : true`) or that re-test a value
200
+ // they could simply fall through to (`a ? a : b`). These are the flat
201
+ // sibling of the nested ternaries `no-nested-ternary` already bans here:
202
+ // punctuation-heavy expressions that dress up a plain boolean (or the
203
+ // condition itself) as a branch, exactly the "clever but unreadable"
204
+ // shortcut this config exists to prevent and one AI assistants reach for
205
+ // often. The explicit form — the condition itself, a negation, or a real
206
+ // `if` — keeps the intent legible. The rule is auto-fixable, so consumers
207
+ // can adopt it with `eslint --fix`. `defaultAssignment: false` extends the
208
+ // check to the `a ? a : b` default-value idiom as well.
209
+ 'no-unneeded-ternary': ['error', { defaultAssignment: false }],
210
+ // Require the `default` clause of a `switch` to come last. A `default` matches
211
+ // only when no `case` does, so its precedence is independent of where it sits
212
+ // — but a `default` written *before* later cases reads as if those cases were
213
+ // unreachable, and a mid-`switch` `default` that omits `break` silently falls
214
+ // through into the cases below it. Both are the "looks one way, behaves
215
+ // another" footgun this config exists to surface, and a shape an AI assistant
216
+ // emitting a `switch` can easily introduce. Pinning `default` to the end keeps
217
+ // its order-independent meaning legible. It sits with the bundled `switch-case`
218
+ // rules already enabled here. It is not in `eslint:recommended`, so it is
219
+ // enabled explicitly, and it is not auto-fixable because moving a clause that
220
+ // omits `break` could change behavior — only the author can reorder safely.
221
+ 'default-case-last': 'error',
222
+ // Require strict equality (=== / !==). Loose equality performs implicit type
223
+ // coercion, exactly the kind of "clever" shortcut this config exists to
224
+ // prevent. Enforcing it in the shared config means consumers no longer have
225
+ // to re-add it on top of the package.
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 }],
238
+ // Disallow reassigning function parameters and mutating their properties.
239
+ // Reassigning a parameter decouples it from the caller's argument and hides the
240
+ // function's real inputs; mutating a parameter's properties causes
241
+ // action-at-a-distance bugs where a callee silently rewrites an object the
242
+ // caller still holds. Both undermine this config's explicit-over-clever,
243
+ // immutability-leaning stance, so treat parameters as read-only here.
244
+ 'no-param-reassign': ['error', { props: true }],
245
+ // Require every parameter with a default value to come after all parameters
246
+ // without one. A default can only ever take effect when the argument is
247
+ // `undefined`, so a default placed before a required parameter
248
+ // (`f(a = 1, b)`) is unreachable in practice: the caller cannot skip `a` to
249
+ // reach `b`, they must pass `f(undefined, x)` to use the default — at which
250
+ // point the default documents an API the call sites cannot actually use.
251
+ // It is almost always a mistake for the parameter order, not a deliberate
252
+ // signature, and exactly the kind of plausible-but-wrong shape an AI
253
+ // assistant emits when it bolts a default onto the first convenient
254
+ // parameter. This sits with the parameter-discipline rules already here
255
+ // (`no-param-reassign` directly above): keep a function's inputs honest and
256
+ // its signature meaningful. It is not in `eslint:recommended`, so it is
257
+ // enabled explicitly. The rule is not auto-fixable because reordering
258
+ // parameters would silently break every call site, so only the author can
259
+ // decide whether the default or the order was wrong.
260
+ 'default-param-last': 'error',
261
+ // Require `const` for bindings that are never reassigned. This is the
262
+ // local-binding counterpart to `no-param-reassign`: together they extend the
263
+ // config's immutability-leaning stance from parameters to every variable. A
264
+ // `let` that is never reassigned misleads readers into expecting a mutation
265
+ // that never comes; `const` documents fixed intent up front. The rule is
266
+ // auto-fixable (`eslint --fix`), so adoption is cheap. `destructuring: 'all'`
267
+ // only flags a destructuring pattern when every introduced binding could be
268
+ // `const`, leaving mixed const/let destructuring alone.
269
+ 'prefer-const': ['error', { destructuring: 'all' }],
270
+ // Disallow shorthand type coercions such as `!!value`, `+str`, `1 * x`,
271
+ // `'' + n` and `~str.indexOf(...)`. These are the sibling of the loose
272
+ // equality `eqeqeq` already bans here: terse tricks that hide an implicit
273
+ // type conversion behind punctuation, exactly the "clever but unreadable"
274
+ // shortcut this config exists to prevent and one AI assistants reach for
275
+ // often. Requiring the explicit form (`Boolean(value)`, `Number(str)`,
276
+ // `String(n)`) keeps the intended conversion legible. The rule is
277
+ // auto-fixable, so consumers can adopt it with `eslint --fix`.
278
+ 'no-implicit-coercion': ['error', { allow: [] }],
279
+ // Require an explicit radix argument to `parseInt` — `parseInt(str, 10)`,
280
+ // never `parseInt(str)`. With the base omitted, `parseInt` infers it from the
281
+ // string: a leading `0x` is read as hex and, depending on the engine, a
282
+ // leading `0` can be read as octal, so `parseInt('0x10')` is `16` and
283
+ // `parseInt(userInput)` silently parses in a base the author never chose. The
284
+ // wrong-number result type-checks fine (it is still a `number`) and only the
285
+ // data flow is broken — exactly the quiet, plausible-but-wrong shape this
286
+ // config exists to surface, and the same class of *implicit* behavior it
287
+ // already bans via `eqeqeq` and `no-implicit-coercion` directly above.
288
+ // Forcing the base makes the parse deterministic and the intent explicit. The
289
+ // rule is not auto-fixable because only the author knows which base was meant.
290
+ radix: ['error', 'always'],
291
+ // Require template literals instead of string concatenation. Building a
292
+ // string by chaining `+` (`'Hello ' + name + '!'`) scatters the literal
293
+ // text across operators, makes the final shape hard to read, and leans on
294
+ // the same implicit coercion `no-implicit-coercion` already bans whenever a
295
+ // non-string operand sneaks in. A template literal (`` `Hello ${name}!` ``)
296
+ // keeps the literal text and the interpolated values visually aligned, which
297
+ // is exactly the explicit, readable form this config favors and one AI
298
+ // assistants frequently skip. The rule is auto-fixable, so consumers can
299
+ // adopt it with `eslint --fix`.
300
+ 'prefer-template': 'error',
301
+ // Disallow concatenating two string literals — `'Hello ' + 'World'`,
302
+ // `'a' + 'b' + 'c'`, or a literal split across lines with `+`. The operands
303
+ // are known at author time, so the `+` does nothing the source could not say
304
+ // directly: it is pure punctuation that hides a single constant string behind
305
+ // an operator and invites the reader to wonder whether a variable was meant.
306
+ // It is the static sibling of the `prefer-template` rule just above — that one
307
+ // pushes runtime interpolation onto template literals, this one removes the
308
+ // join entirely when there is no value to interpolate — and it leans on the
309
+ // same implicit-`+` surface the config already narrows with `prefer-template`,
310
+ // `no-implicit-coercion` and `@typescript-eslint/restrict-plus-operands`.
311
+ // Collapsing the pieces into one literal (or a template literal when the line
312
+ // length is the only reason for the split) is the explicit, readable form this
313
+ // config favors and one AI assistants frequently skip. The rule is not
314
+ // auto-fixable because only the author knows whether the two pieces were meant
315
+ // to be one literal or a refactor left a variable behind.
316
+ 'no-useless-concat': 'error',
317
+ // Disallow `.bind()` on a function that never references `this` (and has no
318
+ // bound arguments) — `function () { return 1 }.bind(this)`,
319
+ // `(() => x).bind(obj)`, `handler.bind(this)` where `handler` ignores `this`.
320
+ // The `.bind()` does nothing: it allocates a new wrapper function on every
321
+ // evaluation and returns one that behaves identically to the original, so the
322
+ // call is pure overhead that also misleads the reader into thinking the
323
+ // receiver matters when it does not. It is the same "looks meaningful but is
324
+ // dead" clutter the `no-useless-return`, `no-useless-assignment` and
325
+ // `no-useless-concat` rules already remove here, and exactly the reflexive
326
+ // `.bind(this)` an AI assistant appends to a callback by habit, whether or not
327
+ // the body uses `this`. Dropping the bind leaves the explicit, allocation-free
328
+ // form this config favors. The rule is auto-fixable, so consumers can adopt it
329
+ // with `eslint --fix`; it is not in `eslint:recommended`, so it is enabled
330
+ // explicitly here.
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',
356
+ // Require `Object.hasOwn(obj, key)` over the legacy ways of asking the same
357
+ // question. The two forms it replaces are each a footgun: calling
358
+ // `obj.hasOwnProperty(key)` directly breaks the moment `obj` has a `null`
359
+ // prototype (`Object.create(null)`, many map-like objects) or shadows the
360
+ // method with an own `hasOwnProperty` field — it throws or returns the wrong
361
+ // answer — which is why `no-prototype-builtins` already bans it; and the safe
362
+ // workaround, `Object.prototype.hasOwnProperty.call(obj, key)`, is a long,
363
+ // punctuation-heavy incantation that hides a one-word intent behind prototype
364
+ // plumbing. `Object.hasOwn` is the single explicit, prototype-safe spelling of
365
+ // "does this object own this key", so it fits this config's
366
+ // explicit-over-clever, correctness-leaning stance and removes the boilerplate
367
+ // adopters would otherwise reach for. It is auto-fixable (`eslint --fix`) and
368
+ // available on every supported runtime (Node >= 20, ES2022), so adoption is
369
+ // free.
370
+ 'prefer-object-has-own': 'error',
371
+ // Disallow `var`. `var` declarations are function-scoped and hoisted, so
372
+ // they leak out of the block they appear to belong to and read as
373
+ // initialized `undefined` before their declaration runs — producing
374
+ // order-dependent bugs that block-scoped `let`/`const` make impossible.
375
+ // `var` is exactly the legacy shortcut an AI assistant trained on older
376
+ // code reaches for, which puts it squarely in scope for this config's
377
+ // explicit-over-clever, AI-safety stance. It is also the foundation the
378
+ // `prefer-const` rule builds on. The rule is auto-fixable.
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',
392
+ // Require object literal shorthand for properties and methods, so
393
+ // `{ value: value }` collapses to `{ value }` and `{ run: function () {} }`
394
+ // to `{ run() {} }`. The longhand forms carry no extra meaning — they are
395
+ // pure boilerplate that an AI assistant trained on older code routinely
396
+ // emits, and the duplicated `value: value` name is a common spot for a
397
+ // copy-paste typo that the shorthand removes entirely. Enforcing one
398
+ // canonical object form keeps literals legible and consistent, matching
399
+ // this config's explicit-over-clever, low-noise stance. The rule is
400
+ // auto-fixable, so consumers can adopt it with `eslint --fix`.
401
+ 'object-shorthand': ['error', 'always'],
402
+ // Require the compound assignment shorthand (`x += 1`, `total *= rate`)
403
+ // wherever a variable is assigned the result of an operation on itself, so
404
+ // `x = x + 1` is rewritten to `x += 1`. The longhand form names the target
405
+ // twice — once on each side of the `=` — and that duplicated operand is a
406
+ // silent typo site: `total = totals + 1` reads as a self-update but quietly
407
+ // assigns from a *different* variable, a bug type checking cannot catch when
408
+ // both names are in scope. Collapsing to `+=` removes the
409
+ // second spelling of the name entirely, so the mismatch becomes impossible to
410
+ // write. This is the assignment sibling of the `object-shorthand` rule just
411
+ // above (which removes the same duplicated-name typo site in `{ value: value }`)
412
+ // and fits this config's explicit-over-clever, low-noise, correctness-leaning
413
+ // stance: one canonical spelling of "update this in place". The longhand is
414
+ // also exactly what an AI assistant trained on older code reaches for. The
415
+ // rule is auto-fixable, so consumers can adopt it with `eslint --fix`.
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
+ ],
443
+ // Disallow chained assignment expressions such as `a = b = c = 0`. Chaining
444
+ // collapses several writes into one expression: the value flows right-to-left
445
+ // through bindings that have nothing to do with each other, so a reader has to
446
+ // unwind the chain to see how many variables changed and to what. It also
447
+ // quietly couples those variables — touching the chain later risks rewriting
448
+ // more than intended — which runs against the immutability-leaning stance the
449
+ // `prefer-const`, `no-param-reassign` and `no-var` rules above already set
450
+ // here. It is exactly the terse, "clever" shortcut this config exists to
451
+ // prevent and one AI assistants emit when packing initialization onto a single
452
+ // line. Writing each assignment on its own statement keeps the data flow
453
+ // explicit.
454
+ 'no-multi-assign': 'error',
455
+ // Disallow `${...}` placeholders inside ordinary string literals
456
+ // (`'Hello ${name}'`, `"total: ${count}"`). Template-literal interpolation
457
+ // only works inside backticks; the moment the quotes are single or double,
458
+ // `${name}` is just literal text and the value is silently never inserted —
459
+ // a bug that type-checking cannot catch because the result is still a valid
460
+ // `string`. Mixing up the quote character is exactly the kind of silent
461
+ // mistake an AI assistant emits when stitching messages together, which puts
462
+ // it squarely in scope for this config's correctness-and-clarity stance. It
463
+ // is the natural companion to the explicit-conversion rules
464
+ // (`no-implicit-coercion`, `eqeqeq`) above: it keeps string building honest
465
+ // by forcing a real template literal whenever interpolation is intended.
466
+ 'no-template-curly-in-string': 'error',
467
+ // Disallow the `Object` constructor (`new Object()` and `Object()`) in favor
468
+ // of the `{}` object literal. The constructor form is a strictly more verbose,
469
+ // more indirect way to do exactly what the literal does — and a trap: passing
470
+ // a single non-object argument makes `Object(x)` return *that* value (or a
471
+ // wrapper), so the call quietly stops creating a fresh object at all. The
472
+ // literal has no such ambiguity. This is the object-creation sibling of the
473
+ // wrapper-constructor and coercion bans this config already sets (`Number`,
474
+ // `String`, `!!x`): prefer the plain literal/value over a constructor call
475
+ // dressed up as one. `new Object()` is also a legacy idiom an AI assistant
476
+ // trained on older code reaches for, which puts it squarely in this config's
477
+ // explicit-over-clever, AI-safety scope. The rule is auto-fixable, so
478
+ // consumers can adopt it with `eslint --fix`.
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',
495
+ // Require object spread (`{ ...a, ...b }`) over `Object.assign({}, a, b)` when
496
+ // the call is building a brand-new object (its first argument is an object
497
+ // literal). The `Object.assign({}, ...)` form is a more indirect, punctuation-
498
+ // heavy spelling of the same intent: a reader has to recognize the empty `{}`
499
+ // accumulator and the mutate-and-return semantics just to conclude "this makes
500
+ // a fresh, merged object", which the spread states directly. It is the
501
+ // object-merging sibling of the `no-object-constructor` ban right above —
502
+ // both reject an indirect construction idiom in favor of the plain literal —
503
+ // and of `object-shorthand`, which already pushes object literals toward their
504
+ // most legible form. `Object.assign` onto an existing target (`Object.assign(
505
+ // target, src)`) mutates and is left alone, so only the new-object case is
506
+ // flagged. The rule is auto-fixable, so consumers can adopt it with
507
+ // `eslint --fix`.
508
+ 'prefer-object-spread': 'error',
509
+ // Require a `return` from every array-method callback that is expected to
510
+ // produce one (`map`, `filter`, `reduce`, `every`, `some`, `find`, `sort`,
511
+ // `flatMap`, ...). A callback that falls off the end returns `undefined`, so
512
+ // `arr.map(x => { doSomething(x) })` silently yields an array of `undefined`
513
+ // and `arr.filter(x => { x.active })` keeps every element — the kind of
514
+ // quiet, wrong-result bug that type checking will not catch and that AI
515
+ // assistants emit when they reach for a brace body and forget the `return`.
516
+ // Unlike most rules here this is a correctness check, not a style
517
+ // preference, which is why it is not covered by `eslint:recommended` and is
518
+ // enabled explicitly. `checkForEach: true` flags the inverse mistake —
519
+ // returning a value from `forEach`, where the result is discarded — which
520
+ // usually means `map` was intended. The rule is not auto-fixable because
521
+ // only the author knows what each callback should return.
522
+ 'array-callback-return': [
523
+ 'error',
524
+ { allowImplicit: false, checkForEach: true },
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',
545
+ // Only allow throwing real `Error` objects — reject `throw 'boom'`,
546
+ // `throw { code: 500 }`, `throw 42` and any other non-Error value. A thrown
547
+ // string or plain object carries no stack trace, so the failure surfaces with
548
+ // no record of where it came from, and it breaks every `catch (e)` that does
549
+ // `e instanceof Error` or reads `e.message`/`e.stack` — the consumer's error
550
+ // handling silently misfires. Throwing a bare literal is exactly the terse
551
+ // shortcut an AI assistant emits when it wants to bail out fast, which puts it
552
+ // squarely in this config's correctness-over-clever, AI-safety scope, and it
553
+ // pairs with the bundled `error/*` rules that already shape how errors are
554
+ // declared. Unlike most rules here it catches an outright correctness
555
+ // mistake, not a style preference, which is why it is enabled explicitly —
556
+ // `eslint:recommended` does not turn it on. The rule is not auto-fixable
557
+ // because only the author knows which `Error` subclass the literal should
558
+ // become.
559
+ 'no-throw-literal': 'error',
560
+ // Disallow comparing a variable to itself (`x === x`, `x !== x`,
561
+ // `i < i`, ...). A self-comparison is either a plain bug — the author meant
562
+ // to compare against a different operand and the result is a constant
563
+ // `true`/`false` that quietly disables a branch — or the deliberate
564
+ // `value !== value` trick for detecting `NaN`. Both belong to the same
565
+ // family the `eqeqeq`, `no-implicit-coercion` and `no-unneeded-ternary`
566
+ // rules above already target: a punctuation-heavy expression whose real
567
+ // intent is hidden, exactly the "clever but unreadable" shortcut this config
568
+ // exists to prevent and one AI assistants emit when stitching together
569
+ // conditions. Unlike most rules here this also catches an outright
570
+ // correctness mistake, which is why it is enabled explicitly —
571
+ // `eslint:recommended` does not turn it on. The explicit form
572
+ // (`Number.isNaN(value)` for the NaN case, the intended operand otherwise)
573
+ // keeps the check legible. The rule is not auto-fixable because only the
574
+ // author knows which operand was meant.
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',
596
+ // Disallow optional chaining in positions where a short-circuit to
597
+ // `undefined` is immediately used in a way that throws at runtime — member
598
+ // access, a call, arithmetic, spread, `instanceof`, `in`, or destructuring on
599
+ // the result. `(obj?.foo).bar`, `(obj?.fn)()`, `(obj?.count) + 1` and
600
+ // `[...(obj?.list)]` all look guarded, but the `?.` only protects the link it
601
+ // sits on: the moment the chain yields `undefined`, the surrounding `.bar` /
602
+ // `()` / `+` / spread runs against `undefined` and throws a `TypeError`. The
603
+ // author wrote `?.` precisely because the left side can be nullish, so this is
604
+ // an outright correctness bug — the guard is defeated by the very expression
605
+ // wrapped around it — not a style preference. It is exactly the half-applied
606
+ // null-safety an AI assistant emits when it sprinkles `?.` onto one segment of
607
+ // a longer access, which puts it in this config's correctness-over-clever, AI-
608
+ // safety scope alongside `no-throw-literal` and `no-self-compare` above. The
609
+ // fix is to extend the optional chain over the whole access (`obj?.foo?.bar`)
610
+ // or to guard the result before using it; only the author knows which, so the
611
+ // rule is not auto-fixable. `eslint:recommended` does enable this, but this
612
+ // config does not extend that preset, so it is turned on explicitly here. The
613
+ // shared `web` app of the `animals-shop` repo enforced this rule manually; it
614
+ // belongs in the shared config so every consumer is covered.
615
+ 'no-unsafe-optional-chaining': [
616
+ 'error',
617
+ { disallowArithmeticOperators: true },
618
+ ],
619
+ // Disallow computed keys that wrap a literal that could be written plainly —
620
+ // `{ ['name']: x }`, `{ [42]: y }`, or `class { ['method']() {} }`. The
621
+ // bracket syntax exists for keys that must be computed at runtime; using it
622
+ // for a static string or number adds punctuation and a layer of indirection
623
+ // that buys nothing and only makes the reader pause to confirm the key is
624
+ // constant after all. It is the same "clever but pointless" clutter the
625
+ // `no-unneeded-ternary` and `no-implicit-coercion` rules already ban here,
626
+ // and one AI assistants emit when they template object keys mechanically.
627
+ // The plain form (`{ name: x }`, `{ method() {} }`) states intent directly.
628
+ // The rule is auto-fixable, so consumers can adopt it with `eslint --fix`.
629
+ // `enforceForClassMembers: true` extends the check from object literals to
630
+ // class members so both surfaces stay consistent.
631
+ 'no-useless-computed-key': ['error', { enforceForClassMembers: true }],
632
+ // Disallow building functions from strings with the `Function` constructor —
633
+ // `new Function('a', 'b', 'return a + b')` or `Function(...)()`. This is `eval`
634
+ // wearing a different name: the body is an opaque string the parser, the type
635
+ // checker and every reader have to take on faith, it executes in the global
636
+ // scope (so it silently can't see the surrounding closure a reader would
637
+ // expect it to), and feeding it any untrusted input is a direct code-injection
638
+ // hole. There is a plain, safe equivalent for every legitimate use — an actual
639
+ // function literal — so the dynamic form buys nothing but risk. It sits
640
+ // squarely in this config's scope: a footgun an AI assistant reaches for when
641
+ // asked to "build a function dynamically," and one `eslint-plugin-security`
642
+ // does not cover (its eval rule, `detect-eval-with-expression`, flags `eval`
643
+ // and not the `Function` constructor). The rule is not auto-fixable because
644
+ // only the author can restate the string body as real code.
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',
664
+ // Disallow `eval(...)`. It hands a string to the JavaScript engine to parse
665
+ // and run at runtime: the body is opaque to the parser, the type checker and
666
+ // every reader, it can reach and mutate the surrounding scope, and feeding it
667
+ // any untrusted input is the textbook code-injection hole. There is a plain,
668
+ // safe equivalent for every legitimate use, so the dynamic form buys nothing
669
+ // but risk. This is the direct-`eval` companion to the `no-new-func` ban right
670
+ // above (which catches `eval` wearing the `Function`-constructor disguise) —
671
+ // together they close the runtime-string-execution footgun an AI assistant
672
+ // reaches for when asked to "run this code dynamically." It is not in
673
+ // `eslint:recommended`, so it is enabled explicitly. Not auto-fixable: only
674
+ // the author can restate the string as real code.
675
+ 'no-eval': 'error',
676
+ // Disallow the implied-`eval` forms: a string first argument to `setTimeout`,
677
+ // `setInterval`, `setImmediate`, or `execScript`. Passing a string there makes
678
+ // the engine `eval` it on the timer's behalf, so it carries every hazard of
679
+ // `no-eval` above while hiding behind an everyday timer API — `setTimeout('do()',
680
+ // 100)` reads as a scheduled call but is really deferred string execution. The
681
+ // safe form is already the idiomatic one: pass a function, `setTimeout(() =>
682
+ // do(), 100)`. This pairs with `no-eval` to cover the indirect channel the
683
+ // direct ban misses. Not auto-fixable: only the author can turn the string
684
+ // into the intended callback.
685
+ 'no-implied-eval': 'error',
686
+ // Disallow `javascript:` URLs — `href = 'javascript:void(0)'`,
687
+ // `window.location = 'javascript:doThing()'`, a `<a href="javascript:...">`.
688
+ // The string after the `javascript:` scheme is handed to the engine and run
689
+ // exactly as `eval` would run it, so it carries every hazard of `no-eval` and
690
+ // `no-implied-eval` above — opaque to the parser, the type checker and every
691
+ // reader, and a code-injection hole the moment any of it comes from untrusted
692
+ // input — while wearing the everyday disguise of a URL. It is the third
693
+ // channel in the string-executes-as-code family this config already bans:
694
+ // `no-eval` (direct), `no-new-func` (the `Function` constructor) and
695
+ // `no-implied-eval` (string-bodied timers); `no-script-url` closes the URL
696
+ // channel the other three miss. There is a plain, safe equivalent for every
697
+ // legitimate use — an `onClick`/event handler, `href="#"` with
698
+ // `preventDefault`, or a real function — so the `javascript:` form buys
699
+ // nothing but risk, and it is exactly the placeholder shortcut an AI assistant
700
+ // emits for a "do-nothing" link. It is not in `eslint:recommended`, so it is
701
+ // enabled explicitly. Not auto-fixable: only the author can replace the URL
702
+ // with the intended handler.
703
+ 'no-script-url': 'error',
704
+ // Disallow stray `console` calls, allowing only `console.warn` and
705
+ // `console.error`. A bare `console.log` is almost always leftover debugging:
706
+ // it slips through review, ships to production, leaks internal state into logs
707
+ // and slows hot paths, all without any type error or runtime failure to flag
708
+ // it — exactly the quiet, wrong-to-ship mistake this config exists to catch
709
+ // and one AI assistants emit liberally while "checking what a value is." The
710
+ // `allow: ['warn', 'error']` exception keeps intentional diagnostics — the two
711
+ // `console` channels meant for surfacing problems — available, so the rule
712
+ // targets only the throwaway logging. Consumers that genuinely build a CLI can
713
+ // relax it for their entry points; for everyone else it is a zero-config
714
+ // guard. The rule is not auto-fixable because only the author knows whether a
715
+ // given `console.log` should be deleted or promoted to a real log channel.
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',
746
+ // Require a regex literal (`/\d+/`) instead of the `RegExp` constructor with
747
+ // a string argument (`new RegExp('\\d+')`, `RegExp('\\d+')`) when the pattern
748
+ // is a static string. The string form forces every backslash to be escaped
749
+ // twice — once for the string literal and once for the regex engine — so
750
+ // `\d` has to be written `\\d`, `\.` becomes `\\.`, and a single missed
751
+ // backslash silently changes what the pattern matches without any error.
752
+ // The literal needs none of that doubling and is checked for validity at
753
+ // parse time rather than when the constructor runs, so a malformed pattern
754
+ // surfaces immediately instead of throwing on first use. This is the
755
+ // regex-shaped sibling of the constructor bans this config already ships —
756
+ // `no-object-constructor`, `no-new-wrappers`, `no-new-func` — all of which
757
+ // reject a constructor call dressed up as a literal/value; reaching for
758
+ // `new RegExp('...')` on a constant pattern is exactly the legacy,
759
+ // double-escaping-prone idiom an AI assistant emits when asked to "build a
760
+ // regex." It is not in `eslint:recommended`, so it is enabled explicitly.
761
+ // `disallowRedundantWrapping: true` also flags `new RegExp(/foo/)` — wrapping
762
+ // a literal that is already a regex — which is pure redundancy. The dynamic
763
+ // `new RegExp(variable)` case, where the pattern genuinely is not known until
764
+ // runtime, is left untouched. The rule is auto-fixable for the simple cases,
765
+ // so consumers can adopt much of it with `eslint --fix`.
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',
45
839
  }
46
840
 
47
841
  // Shared no-restricted-syntax rules for both JS and TS
@@ -76,7 +870,7 @@ const sharedRestrictedSyntax = [
76
870
  message:
77
871
  'Exporting from external libraries is not allowed. Only re-export from relative paths or scoped packages.',
78
872
  },
79
- allRules.noProcessEnvPropertiesConfig,
873
+ allRules.noProcessEnvironmentPropertiesConfig,
80
874
  allRules.noExportSpecifiersConfig,
81
875
  ...allRules.noDefaultClassExportRules,
82
876
  ]
@@ -97,32 +891,91 @@ const tsOnlyRestrictedSyntax = [
97
891
  ]
98
892
 
99
893
  const config = defineConfig([
100
- // Global ignores for non-JS/TS files and build outputs
894
+ // Global ignores for non-JS/TS files and build outputs.
895
+ //
896
+ // Build-output globs are prefixed with `**/` so they match nested package
897
+ // directories, not just the repository root. A bare `dist/**` only ignores
898
+ // a top-level `dist/`, which means in a monorepo every `packages/*/dist/**`
899
+ // (and the same for `build`, `coverage`, etc.) is still linted and floods
900
+ // adopters with errors on generated code. The recursive form ignores those
901
+ // outputs wherever they appear, matching the monorepo support this config
902
+ // documents.
101
903
  {
904
+ name: 'agent/ignores',
102
905
  ignores: [
103
906
  '**/*.json',
104
907
  '**/*.md',
105
908
  '**/*.yaml',
106
909
  '**/*.yml',
107
- 'dist/**',
108
- 'coverage/**',
910
+ '**/dist/**',
911
+ '**/build/**',
912
+ '**/coverage/**',
913
+ '**/.next/**',
914
+ '**/out/**',
915
+ // Storybook's static build output. This config ships first-class
916
+ // Storybook support (`eslint-plugin-storybook` + `configs/storybook.js`),
917
+ // so adopters routinely run `storybook build`, which emits compiled,
918
+ // minified bundles into `storybook-static/`. Linting that generated
919
+ // output is never useful and floods the report, so ignore it like the
920
+ // other build dirs above.
921
+ '**/storybook-static/**',
922
+ // Turborepo's local task cache. In the monorepos this config targets,
923
+ // `turbo` writes hashed cache artifacts into `.turbo/`; they are
924
+ // generated, not source, and must not be linted.
925
+ '**/.turbo/**',
109
926
  ],
110
927
  },
928
+ // Flag `eslint-disable` directives that no longer suppress anything. Stale
929
+ // suppressions hide the fact that a rule is now satisfied (or was renamed)
930
+ // and quietly widen the set of unchecked code over time, which runs against
931
+ // this config's explicit-over-clever goal. Reporting them as errors keeps
932
+ // every suppression honest and removable.
933
+ {
934
+ name: 'agent/linter-options',
935
+ linterOptions: {
936
+ reportUnusedDisableDirectives: 'error',
937
+ },
938
+ },
111
939
  // Global plugin definitions
112
940
  {
941
+ name: 'agent/plugins',
113
942
  plugins,
114
943
  },
115
944
  {
945
+ name: 'agent/react-hooks',
116
946
  rules: {
117
947
  'react-hooks/rules-of-hooks': 'error',
118
948
  'react-hooks/exhaustive-deps': 'warn',
119
949
  },
120
950
  },
121
951
  js.configs.recommended,
952
+ unicorn.configs.all,
953
+ // Disable unicorn rules that are too opinionated or conflict with this
954
+ // codebase's conventions. These are turned off globally after
955
+ // `unicorn.configs.all` enables them.
956
+ {
957
+ name: 'agent/unicorn-overrides',
958
+ rules: {
959
+ // Prevents `class`-prefixed identifiers like `classExportPlugin`,
960
+ // `classDeclaration`, etc. — common in ESLint plugin code where `class`
961
+ // is part of a domain concept, not a keyword accident.
962
+ 'unicorn/no-keyword-prefix': 'off',
963
+ // Disallows `null` in favor of `undefined`. TypeScript APIs use `null`
964
+ // intentionally (e.g. `null` vs `undefined` in type narrowing).
965
+ 'unicorn/no-null': 'off',
966
+ // Prevents passing a function reference directly to `.map(fn)`. The
967
+ // direct reference form is idiomatic and readable.
968
+ 'unicorn/no-array-callback-reference': 'off',
969
+ // Enforces specific import styles for some packages (e.g. named vs
970
+ // default). Too opinionated for this config's consumers.
971
+ 'unicorn/import-style': 'off',
972
+ },
973
+ },
122
974
  ...tseslint.configs.strictTypeChecked,
123
975
  ...tseslint.configs.stylisticTypeChecked,
124
976
  {
125
- files: ['**/*.{ts,tsx}'],
977
+ name: 'agent/typescript-parser-options',
978
+ files: ['**/*.{ts,tsx,mts,cts}'],
126
979
  languageOptions: {
127
980
  parserOptions: {
128
981
  projectService: true,
@@ -132,10 +985,32 @@ const config = defineConfig([
132
985
  {
133
986
  files: ['**/*.{js,jsx,mjs,cjs}'],
134
987
  ...tseslint.configs.disableTypeChecked,
988
+ name: 'agent/javascript-disable-type-checked',
135
989
  },
136
990
  earlyReturn.configs.recommended,
137
991
  jsdoc.configs['flat/recommended-typescript-error'],
138
- { plugins: { 'switch-case': switchCase }, ...switchCase.configs.recommended },
992
+ // The jsdoc recommended preset only requires JSDoc on FunctionDeclaration by
993
+ // default. Treat class declarations the same as functions so exported
994
+ // classes are also required to be documented.
995
+ {
996
+ name: 'agent/jsdoc-require-class-jsdoc',
997
+ rules: {
998
+ 'jsdoc/require-jsdoc': [
999
+ 'error',
1000
+ {
1001
+ require: {
1002
+ ClassDeclaration: true,
1003
+ FunctionDeclaration: true,
1004
+ },
1005
+ },
1006
+ ],
1007
+ },
1008
+ },
1009
+ {
1010
+ plugins: { 'switch-case': switchCase },
1011
+ ...switchCase.configs.recommended,
1012
+ name: 'agent/switch-case',
1013
+ },
139
1014
 
140
1015
  // Base plugin strict configs (error, default, guard-clauses)
141
1016
  ...basePluginsConfig,
@@ -173,6 +1048,7 @@ const config = defineConfig([
173
1048
  ...jsxClassname.configs.strict,
174
1049
  files: ['**/*.{tsx,jsx}'],
175
1050
  ignores: ['**/*.stories.{js,jsx,ts,tsx}'],
1051
+ name: 'agent/jsx-classname-strict',
176
1052
  },
177
1053
 
178
1054
  // Final overrides (index files, switch case, length rules)