eslint-config-agent 3.0.3 → 3.0.5

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