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
@@ -2,6 +2,412 @@ import { noExplicitAnyConfig } from './no-explicit-any/index.js'
2
2
 
3
3
  export const typescriptEslintRules = {
4
4
  ...noExplicitAnyConfig,
5
+ // `strictTypeChecked` (which this config extends) enables
6
+ // `@typescript-eslint/only-throw-error` — the type-aware replacement for the
7
+ // core `no-throw-literal` rule that catches additional cases (e.g. throwing a
8
+ // variable typed as `string | Error`). The core rule is still active in
9
+ // `sharedRules` and, because `sharedRules` is spread into the TypeScript
10
+ // config *after* the preset, it overrides the preset's own `no-throw-literal:
11
+ // 'off'`. Turning the core rule off here (in `typescriptEslintRules`, which
12
+ // is spread after `sharedRules`) ensures TypeScript files are checked only by
13
+ // the type-aware rule so there is no double-reporting of the same violation.
14
+ // The core rule remains active for `.js`/`.jsx` files via `sharedRules`,
15
+ // where the type-aware version does not run.
16
+ 'no-throw-literal': 'off',
17
+ // `strictTypeChecked` (which this config extends) includes `recommended`,
18
+ // which enables `@typescript-eslint/no-useless-constructor` for TypeScript
19
+ // files. The core `no-useless-constructor` rule is active in `sharedRules`
20
+ // for JavaScript files. Turning the core rule off here ensures TypeScript
21
+ // files are covered only by the TypeScript-aware variant, preventing
22
+ // double-reporting on the same constructor.
23
+ 'no-useless-constructor': 'off',
24
+ // `stylisticTypeChecked` (which this config extends) enables
25
+ // `@typescript-eslint/prefer-nullish-coalescing`, recommending `??` over `||`
26
+ // for nullable-type checks. However, this config deliberately bans the `??`
27
+ // operator via a `no-restricted-syntax` rule (see `noNullishCoalescingConfig`)
28
+ // in favour of explicit `!== null && !== undefined` checks. With both active,
29
+ // a user writing `x || 'default'` (where `x` is nullable) gets
30
+ // `prefer-nullish-coalescing` telling them to use `??`, and then
31
+ // `no-restricted-syntax` forbidding the `??` they just added — an
32
+ // unresolvable contradiction. Turning this rule off here lets
33
+ // `no-restricted-syntax` be the single, authoritative constraint: reach for
34
+ // explicit null checks, not `??`.
35
+ '@typescript-eslint/prefer-nullish-coalescing': 'off',
5
36
  '@typescript-eslint/consistent-type-assertions': 'off',
6
37
  '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
38
+ // Force the **property style** (`foo: (x: number) => void`) for every method
39
+ // member of an interface or object type, and forbid the **method shorthand**
40
+ // (`foo(x: number): void`). The two forms look interchangeable but are not:
41
+ // TypeScript deliberately exempts method-shorthand signatures from
42
+ // `strictFunctionTypes` and checks them *bivariantly* (a documented language
43
+ // unsoundness), while property-style signatures are checked *contravariantly*
44
+ // (sound). In practice a method-shorthand declaration silently accepts an
45
+ // incompatible callback or override that the property style would reject —
46
+ // the incompatibility passes type-checking and only surfaces as a wrong call
47
+ // at runtime. That is exactly the "looks typed, fails at runtime" gap this
48
+ // config exists to close, and it is the signature-side companion of the
49
+ // already-enabled `no-non-null-assertion` and `require-array-sort-compare`
50
+ // rules. The rule is *not* in typescript-eslint's `strictTypeChecked` preset
51
+ // this config extends, so it must be turned on explicitly. It is
52
+ // **auto-fixable** (`eslint --fix`), so adoption costs nothing. Both
53
+ // `tupe12334/zod-utils` and `tupe12334/tools-view` already re-add it by hand
54
+ // on top of the shared config; promoting it here removes that copy-paste and
55
+ // covers every downstream consumer.
56
+ '@typescript-eslint/method-signature-style': ['error', 'property'],
57
+ // Require a `case` for every member of the union/enum a `switch` discriminates
58
+ // on. This is the type-aware completion of the `no-restricted-syntax` ban on
59
+ // `default` cases this config already ships ("Default cases are not allowed in
60
+ // switch statements. Handle all possible cases explicitly."): banning the
61
+ // catch-all only matters if something then proves the remaining cases are
62
+ // actually exhaustive. Without this rule a `switch` over a string-literal
63
+ // union or enum can silently miss a member — the missing branch just falls
64
+ // through to nothing — and, crucially, adding a new member to the union later
65
+ // does NOT flag the now-incomplete switch, so the bug ships unnoticed. That
66
+ // silent-fallthrough-on-extension footgun is exactly the kind of mistake AI
67
+ // assistants introduce when they widen a union but forget a call site. With
68
+ // the rule on, the omission is a compile-time-shaped error pointing at the
69
+ // exact missing member.
70
+ //
71
+ // `requireDefaultForNonUnion: false` keeps it aligned with the default-case
72
+ // ban: switches over open types like `number`/`string` are not forced to add
73
+ // the `default` branch this config forbids. `allowDefaultCaseForExhaustiveSwitch`
74
+ // is left at its default (`true`) so the rule never fights that ban. The rule
75
+ // is type-aware and runs under the `projectService` parser options already
76
+ // configured for `.ts`/`.tsx` files.
77
+ '@typescript-eslint/switch-exhaustiveness-check': [
78
+ 'error',
79
+ {
80
+ requireDefaultForNonUnion: false,
81
+ },
82
+ ],
83
+ // Require a compare function when sorting anything that is not an array of
84
+ // strings. `Array.prototype.sort` (and `toSorted`) coerce every element to a
85
+ // string and compare the UTF-16 code units, so `[10, 2, 1].sort()` yields
86
+ // `[1, 10, 2]` and `[2, 1, 10].toSorted()` yields `[1, 10, 2]` — the numbers
87
+ // come back in the wrong order. This is a silent correctness bug the type
88
+ // checker cannot catch (the call is perfectly typed) and exactly the kind of
89
+ // shortcut an AI assistant emits when it reaches for `.sort()` on a numeric
90
+ // array, which puts it squarely in scope for this config's
91
+ // explicit-over-clever, bug-prevention stance. `ignoreStringArrays: true`
92
+ // keeps the rule silent on `string[]` (where the default lexicographic order
93
+ // is what the author wants), so it only fires on the cases that are actually
94
+ // wrong and carries near-zero false-positive cost. It is type-aware, hence it
95
+ // lives in the TypeScript-only rule set rather than `sharedRules`.
96
+ '@typescript-eslint/require-array-sort-compare': [
97
+ 'error',
98
+ { ignoreStringArrays: true },
99
+ ],
100
+ // Force `import type { ... }` for imports used only as types. A type-only
101
+ // import is erased at compile time, so writing it as a value import leaves a
102
+ // binding that looks like a runtime dependency: it can pull a module (and its
103
+ // side effects) into the emitted JS even though nothing actually uses the
104
+ // value, and it breaks under TypeScript's `verbatimModuleSyntax` /
105
+ // `isolatedModules`. Making the type/value split explicit keeps the emitted
106
+ // graph honest and the intent of every import legible — squarely in line with
107
+ // this config's explicit-over-clever stance. It is exactly the distinction an
108
+ // AI assistant blurs when it merges a type and a value into one import line.
109
+ // The rule is auto-fixable (`eslint --fix`), so adoption is cheap; this is
110
+ // why several downstream repos already re-add it by hand on top of the base
111
+ // config. `fixStyle: 'separate-type-imports'` keeps type and value imports on
112
+ // distinct statements rather than the inline `import { type X }` form.
113
+ '@typescript-eslint/consistent-type-imports': [
114
+ 'error',
115
+ { prefer: 'type-imports', fixStyle: 'separate-type-imports' },
116
+ ],
117
+ // The export-side mirror of `consistent-type-imports`: force `export type
118
+ // { ... }` for any re-export that only carries types. The two rules guard the
119
+ // same invariant from opposite ends of a module — a binding that exists only
120
+ // in type space must never be smuggled through a value statement. A type-only
121
+ // name re-exported with a plain `export { ... }` is erased at compile time,
122
+ // so the value-shaped statement leaves a runtime export edge for something
123
+ // that has no runtime existence: bundlers keep the source module (and its
124
+ // side effects) alive in the emitted graph, and the re-export breaks outright
125
+ // under `verbatimModuleSyntax` / `isolatedModules`, which reject a value
126
+ // export of a type. Splitting type and value re-exports keeps the emitted
127
+ // module graph honest and the public value-vs-type surface of a barrel file
128
+ // legible — exactly the distinction an AI assistant blurs when it folds a
129
+ // type and a value into one `export { ... }` line. The rule is auto-fixable
130
+ // (`eslint --fix`), so adoption is cheap; this is why downstream repos
131
+ // (`block-no-verify`, `tools-view`) already re-add it by hand on top of the
132
+ // base config. Left at its default `fixMixedExportsWithInlineTypeSpecifier:
133
+ // false` so the fixer emits a separate `export type { ... }` statement rather
134
+ // than the inline `export { type X }` form, matching the
135
+ // `separate-type-imports` choice on the import side.
136
+ '@typescript-eslint/consistent-type-exports': 'error',
137
+ // Forbid the inline `import { type X }` mixed-qualifier form and force a
138
+ // separate top-level `import type { X }` statement instead. This is the
139
+ // erase-side guarantee that completes the type-import trio already in this
140
+ // config (`consistent-type-imports` + `consistent-type-exports`): those two
141
+ // make every type-only import and re-export *declared* as type-only, and this
142
+ // one makes sure that declaration actually disappears from the emitted JS. An
143
+ // inline `import { type X }` still emits a runtime `import` statement for the
144
+ // module — the `type` qualifier only strips the binding, not the side-effect
145
+ // import — so a module imported solely for its types keeps its source (and any
146
+ // side effects) alive in the bundle, the exact runtime-graph leak the trio is
147
+ // meant to prevent. Rewriting it as a standalone `import type { X }` lets
148
+ // TypeScript drop the whole statement, and it is the form
149
+ // `consistent-type-imports` (configured here with
150
+ // `fixStyle: 'separate-type-imports'`) already produces, so the two rules
151
+ // reinforce one statement style rather than fighting. The rule is auto-fixable
152
+ // (`eslint --fix`), so adoption is free, and it is left out of
153
+ // typescript-eslint's `strictTypeChecked` preset that this config extends, so
154
+ // it must be turned on explicitly — which is why downstream repos (`zod-utils`)
155
+ // already re-add it by hand on top of the base config.
156
+ '@typescript-eslint/no-import-type-side-effects': 'error',
157
+ // Forbid the non-null assertion operator (`value!`). A `!` silently tells the
158
+ // compiler "trust me, this is never null/undefined" and is then erased at
159
+ // build time, so a wrong assumption does not fail at the assertion — it
160
+ // surfaces far away as a runtime "Cannot read properties of undefined" crash,
161
+ // often inside a *consumer's* app. That is the exact failure mode the type
162
+ // system exists to prevent, and `!` is the one operator that opts out of it
163
+ // with zero runtime guard. Forcing an explicit narrowing instead (a
164
+ // `if (x == null) throw`, a `?? fallback`, or a real type guard) keeps the
165
+ // null-safety guarantee honest and the failure loud and local. It is exactly
166
+ // the shortcut an AI assistant reaches for to silence a "possibly undefined"
167
+ // error without handling the case. The rule is deliberately left out of
168
+ // typescript-eslint's `strictTypeChecked` preset that this config extends, so
169
+ // it must be turned on explicitly — which is why several downstream repos
170
+ // (`zod-utils`, `currency-fa`, `block-no-verify`) already re-add it by hand
171
+ // on top of the base config.
172
+ '@typescript-eslint/no-non-null-assertion': 'error',
173
+ // Forbid a declaration from shadowing a name in an outer scope. A shadowed
174
+ // identifier (a nested `value`, `index`, `result` or `error` that hides the
175
+ // outer binding of the same name) reads as if it refers to the outer one
176
+ // while it does not — a classic "I updated/returned the wrong variable" bug
177
+ // and a frequent source of confusing diffs during refactors. It is exactly
178
+ // the kind of accidental reuse an AI assistant introduces when it drops a new
179
+ // block into existing code without checking the surrounding names, which puts
180
+ // it squarely in this config's explicit-over-clever, bug-prevention stance.
181
+ //
182
+ // The core `no-shadow` rule is intentionally left `off` (see `sharedRules` in
183
+ // `index.js`): it false-positives on TypeScript-specific patterns such as a
184
+ // type and a value legitimately sharing a name, and enum members. The
185
+ // typescript-eslint version understands those cases, so it is the documented
186
+ // replacement rather than a second rule fighting the first. It needs no type
187
+ // information, so it adds no parser cost. This is why downstream repos
188
+ // (`zod-utils`, `block-no-verify`) already re-add `@typescript-eslint/no-shadow`
189
+ // by hand on top of the base config — promoting it into the shared rule set
190
+ // removes that copy-paste.
191
+ '@typescript-eslint/no-shadow': 'error',
192
+ // Forbid referencing a `let`/`const`/`class` binding before its textual
193
+ // declaration. Thanks to the Temporal Dead Zone, an early reference to such
194
+ // a binding does not read `undefined` — it throws a `ReferenceError` at
195
+ // runtime, so the file can type-check and look correct while a particular
196
+ // code path (a closure invoked before its outer scope finishes
197
+ // initializing, a module-level `const` referenced by another before both
198
+ // have run) blows up only when actually executed. That looks-right-fails-
199
+ // at-runtime gap is exactly what this config exists to close. Declaration
200
+ // order should be a reliable signal of evaluation order; this rule makes
201
+ // violations a lint error instead of a surprise crash.
202
+ //
203
+ // The core `no-use-before-define` rule is intentionally left `off` (see
204
+ // `sharedRules` in `index.js`): it does not understand TypeScript's hoisted
205
+ // type-level declarations (`interface`, `type`, and `enum` members are safe
206
+ // to reference before their textual position) and would false-positive on
207
+ // them. The typescript-eslint version understands those cases, so it is the
208
+ // documented replacement rather than a second rule fighting the first. It
209
+ // needs no type information, so it adds no parser cost.
210
+ //
211
+ // `functions: false` exempts function declarations, which are fully
212
+ // hoisted (including their body) and safe to call before their textual
213
+ // definition — flagging them would only add noise, not catch a real bug.
214
+ // `classes` and `variables` stay `true` because those bindings are hoisted
215
+ // but left uninitialized (the TDZ), so referencing them early is the actual
216
+ // runtime hazard this rule exists to catch. This mirrors how downstream repo
217
+ // `ameliso-io/web` already configures the rule by hand on top of the base
218
+ // config — promoting it into the shared rule set removes that copy-paste.
219
+ '@typescript-eslint/no-use-before-define': [
220
+ 'error',
221
+ { functions: false, classes: true, variables: true },
222
+ ],
223
+ // Forbid declaring a function inside a loop when that function closes over a
224
+ // binding that changes between iterations. A function created in a loop
225
+ // captures its outer variables *by reference*, not by value, so every closure
226
+ // shares the one binding and, by the time any of them runs, sees that
227
+ // binding's final value — not the per-iteration value the author plainly
228
+ // intended. The textbook bug is registering handlers/timers in a loop
229
+ // (`for (let i = 0; ...) el.onclick = () => use(i)` over a `var i`, or pushing
230
+ // `() => row` closures while a loop reassigns `row`): every callback fires with
231
+ // the last value. The function "looks like it captures this iteration" but does
232
+ // not — the same looks-right-fails-at-runtime mismatch this config exists to
233
+ // catch, and exactly the shortcut an AI assistant emits when it lifts a closure
234
+ // into a loop body without checking what the closure captures. The fix is
235
+ // explicit: bind the per-iteration value (a block-scoped `const` inside the
236
+ // loop, a parameter, or `.map`/`.forEach` whose callback gets a fresh binding),
237
+ // which the rule's safe forms allow. It is not in `eslint:recommended` or in
238
+ // typescript-eslint's `strictTypeChecked`/`stylisticTypeChecked` presets this
239
+ // config extends, so it must be enabled explicitly. The typescript-eslint
240
+ // extension is used in place of the core `no-loop-func` because it understands
241
+ // TypeScript scoping — a reference to a type, an `enum`, or a `const` that
242
+ // cannot change across iterations is not flagged — so it keeps the rule's
243
+ // signal without the false positives the core rule raises in typed code. It
244
+ // needs no type information, so it adds no parser cost.
245
+ '@typescript-eslint/no-loop-func': 'error',
246
+ // Prefer optional chaining (`a?.b?.c`) over a chain of `&&` null-guards
247
+ // (`a && a.b && a.b.c`). The `&&`-chain form evaluates the base expression
248
+ // repeatedly and handles `0`, `""`, and `false` as "missing" — a falsy-value
249
+ // coercion bug the type checker cannot catch because the operator is
250
+ // perfectly typed. Optional chaining short-circuits on `null`/`undefined`
251
+ // only, evaluates the base once, and is the form TypeScript was designed to
252
+ // express. It is also shorter and reads left-to-right with no repeated
253
+ // sub-expressions. The rule is auto-fixable (`eslint --fix`), so adoption is
254
+ // cheap. It lives in typescript-eslint's `stylistic-type-checked` preset but
255
+ // NOT in `strict-type-checked`, which is why this config (extending
256
+ // `strictTypeChecked`) must enable it explicitly — and why a downstream repo
257
+ // (`tools-view`) already re-adds it by hand on top of the base config.
258
+ '@typescript-eslint/prefer-optional-chain': 'error',
259
+ // Require any function that returns a `Promise` to be declared `async`. A
260
+ // plain (non-`async`) function that returns a promise can still throw
261
+ // *synchronously* — anything that runs before the promise is constructed (an
262
+ // argument access, a guard clause, a `JSON.parse`) throws on the call stack,
263
+ // not into the returned promise. A caller that does `fn().catch(...)` or
264
+ // `await fn()` only guards the *rejection* path, so that synchronous throw
265
+ // escapes the intended handler and crashes far from its source — the exact
266
+ // mismatch between "looks async, fails sync" that the type checker cannot see
267
+ // (the signature is `Promise<T>` either way). Marking the function `async`
268
+ // moves every error path into the promise, so one `.catch`/`try-await`
269
+ // contract covers the whole function. This is the type-aware completion of
270
+ // already-enabled `@typescript-eslint/no-floating-promises` (handle the
271
+ // promise you get): this rule makes the returned promise the *only* way the
272
+ // function can fail, that one makes sure the caller actually handles it —
273
+ // together they close the async-hygiene loop. It is exactly the shortcut an
274
+ // AI assistant takes when it returns a
275
+ // `.then()` chain from a non-`async` helper. The rule is type-aware and runs
276
+ // under the `projectService` parser options already configured for `.ts`/
277
+ // `.tsx`, which is why downstream repos (`tools-view`) re-add it by hand on
278
+ // top of the base config.
279
+ '@typescript-eslint/promise-function-async': 'error',
280
+ // Require `return await promise` (never a bare `return promise`) inside a
281
+ // `try`/`catch`, and forbid the redundant `return await` everywhere else.
282
+ // A bare `return promise` from inside a `try` block hands the promise back to
283
+ // the caller *before* it settles, so the enclosing `try`/`catch` (and any
284
+ // `finally`) has already unwound by the time the promise rejects: the
285
+ // rejection sails straight past the local `catch` that was written precisely
286
+ // to handle it, and a `finally` meant to run after the work completes runs
287
+ // too early. The function "looks guarded" but isn't — the same looks-safe,
288
+ // fails-elsewhere mismatch this config already targets — and it is exactly
289
+ // what an AI assistant emits when it wraps an async call in `try`/`catch` but
290
+ // drops the `await` on the `return`. Inserting the `await` keeps the promise's
291
+ // lifetime inside the protected scope, so the `catch` fires and the `finally`
292
+ // ordering is correct; as a bonus the awaited frame stays on the async stack
293
+ // trace instead of being elided, so the error points at the real call site.
294
+ // This is the third side of the async-hygiene triangle this config already
295
+ // builds: `no-floating-promises` (handle the promise you get),
296
+ // `promise-function-async` (route every failure through the returned promise),
297
+ // and now `return-await` (keep that promise inside the handler that guards
298
+ // it). `'in-try-catch'` is the surgical mode — it only requires the `await`
299
+ // where omitting it actually changes `try`/`catch`/`finally` behavior and
300
+ // flags the redundant `return await` everywhere else, so it adds correctness
301
+ // without noise. The rule is auto-fixable (`eslint --fix`) and type-aware,
302
+ // running under the `projectService` parser options already configured for
303
+ // `.ts`/`.tsx`.
304
+ '@typescript-eslint/return-await': ['error', 'in-try-catch'],
305
+ // Flag Promises used in positions where their resolution value cannot be
306
+ // observed — the fourth side of the async-hygiene quadrant already in this
307
+ // config (`no-floating-promises`, `promise-function-async`, `return-await`).
308
+ //
309
+ // Three patterns are caught:
310
+ //
311
+ // 1. `checksVoidReturn` (default `true`): an async function passed where the
312
+ // callback's return type is `void` — the textbook case is
313
+ // `array.forEach(async item => { await work(item) })`. The returned
314
+ // `Promise<void>` is structurally assignable to `void`, so TypeScript's
315
+ // own type-checker never flags it, but the Promise is silently dropped and
316
+ // its rejection becomes an unhandled rejection crash. The fix is an
317
+ // explicit `for...of` loop with `await`, or an `async` wrapper that is
318
+ // then awaited by the caller.
319
+ //
320
+ // 2. `checksConditionals` (default `true`): a Promise used directly in a
321
+ // boolean context — `if (fetchData())` or `while (poll())`. The Promise
322
+ // object itself is always truthy, so the condition never reflects the
323
+ // resolved value; this is almost always a missing `await`.
324
+ //
325
+ // 3. `checksSpreads` (default `true`): `{...asyncFn()}` spreads the Promise
326
+ // object's own properties (none meaningful) instead of the resolved
327
+ // object's properties — a silent data-loss bug.
328
+ //
329
+ // All three are silent: TypeScript does not flag them, the code runs without
330
+ // a syntax error, and the bug surfaces at runtime as either a missing
331
+ // side-effect, an always-true conditional, or an empty object spread. They
332
+ // are exactly the "looks correct, fails at runtime" shortcuts an AI assistant
333
+ // produces when it wraps async work in a sync-looking callback or forgets an
334
+ // `await`. The rule is type-aware, so it runs under the `projectService`
335
+ // parser options already configured for `.ts`/`.tsx` files. Downstream repos
336
+ // (`book-processor`) already promote this rule to `error` on top of the base
337
+ // config — promoting it into the shared rule set removes that copy-paste.
338
+ '@typescript-eslint/no-misused-promises': 'error',
339
+ // Require an explicit initializer for every enum member. Without one,
340
+ // TypeScript assigns implicit numeric values (0, 1, 2, …) based on the
341
+ // member's position in the declaration. Reordering members or inserting a
342
+ // new one in the middle silently changes every subsequent member's numeric
343
+ // value — a breaking API change for any caller that stored or compared the
344
+ // raw numbers. The type-checker cannot flag this: the values are still typed
345
+ // as the enum, not as literal `0`/`1`/`2`, so the mismatch only surfaces at
346
+ // runtime. Writing an explicit initializer (`= 'Up'`, `= 1`) makes the
347
+ // assigned value visible and independent of position: adding or reordering
348
+ // members is now safe by construction, and the intent is legible without
349
+ // counting indices. It is exactly the "looks stable, breaks on extension"
350
+ // footgun AI assistants introduce when they generate an enum and leave the
351
+ // compiler to decide the values — the enum counterpart of the
352
+ // `array-callback-return` and `switch-exhaustiveness-check` rules this
353
+ // config already ships for other "silent missing value" bugs. The rule is
354
+ // not in `strictTypeChecked` or `stylisticTypeChecked`, so it must be
355
+ // enabled explicitly. It is not auto-fixable: only the author knows what
356
+ // each member's value should be.
357
+ '@typescript-eslint/prefer-enum-initializers': 'error',
358
+ // Forbid enum declarations that mix numeric and string member values. TypeScript
359
+ // allows both `Status.Active = 0` (number) and `Status.Name = 'active'`
360
+ // (string) in the same declaration, but the resulting type widens in
361
+ // surprising ways: a mixed enum has no reverse-mapping for string members,
362
+ // exhaustiveness checks over it behave differently depending on TypeScript
363
+ // version, and `Object.values(Status)` returns `[0, 'active']` — a
364
+ // heterogeneous array the caller rarely expects. Combined with the existing
365
+ // `prefer-enum-initializers` rule (which makes every member's value
366
+ // explicit), this rule makes the *kind* of value explicit too: all numbers or
367
+ // all strings, never both. It is exactly the implicit ambiguity an AI
368
+ // assistant introduces when it scaffolds an enum from a mixed-type spec ("id
369
+ // is 0, label is 'active'") without deciding which kind to use. The rule is
370
+ // not in `strictTypeChecked` or `stylisticTypeChecked`, so it must be enabled
371
+ // explicitly. It is not auto-fixable: only the author knows whether to
372
+ // normalize to numbers or strings.
373
+ '@typescript-eslint/no-mixed-enums': 'error',
374
+ // Flag a condition, `&&`/`||` operand, or optional-chain (`?.`) link whose
375
+ // type proves it can never be anything but truthy or never be anything but
376
+ // falsy. A guard like `if (value)` where `value`'s type is a non-nullable
377
+ // object looks like a real null/undefined check, but the type checker has
378
+ // already proven it always passes — the "guard" is dead code that silently
379
+ // hides the fact the author expected a case (`null`, `undefined`, `false`)
380
+ // the type no longer allows, usually because an earlier refactor narrowed
381
+ // the type and left a now-redundant check behind. The inverse (a condition
382
+ // that is always falsy) is worse: the guarded branch is unreachable dead
383
+ // code that looks live. Both are exactly the "looks like a safety check,
384
+ // does nothing" gap this config exists to close, and the type-aware
385
+ // completion of the same always-true/false class of bug
386
+ // `no-self-compare` and `no-constant-binary-expression` already catch by
387
+ // syntax alone. The rule is type-aware, so it runs under the
388
+ // `projectService` parser options already configured for `.ts`/`.tsx`
389
+ // files, and it is why downstream repo `block-no-verify` already re-adds
390
+ // it by hand on top of the base config.
391
+ '@typescript-eslint/no-unnecessary-condition': 'error',
392
+ // Forbid a `private` (TypeScript keyword) or `#hashPrivate` class field or
393
+ // method that is declared and never read or called anywhere in the class
394
+ // body. Because the member is private, there is no legitimate external
395
+ // caller to account for — an unused one is dead code: a leftover from a
396
+ // refactor that forgot to delete it, or worse, a member the author meant to
397
+ // wire up but a typo or missed call site left orphaned, silently doing
398
+ // nothing while the reader assumes it does something.
399
+ //
400
+ // The core `no-unused-private-class-members` rule (already enabled via
401
+ // `eslint:recommended`) only understands ECMAScript `#hashPrivate` fields —
402
+ // it has no notion of TypeScript's `private` keyword, which is the actual
403
+ // dead-code surface in a TypeScript-heavy codebase. The typescript-eslint
404
+ // version is a strict superset (flags both forms) and needs no type
405
+ // information, so it adds no parser cost. This mirrors how
406
+ // `@typescript-eslint/no-shadow` and `@typescript-eslint/no-use-before-define`
407
+ // above already replace their core counterparts. Downstream repo
408
+ // `ameliso-io/web` already re-adds both the core and typescript-eslint
409
+ // variants by hand on top of the base config — promoting the
410
+ // typescript-eslint version into the shared rule set removes that
411
+ // copy-paste.
412
+ '@typescript-eslint/no-unused-private-class-members': 'error',
7
413
  }
@@ -0,0 +1,57 @@
1
+ # require-spec-file-tsx
2
+
3
+ Require a sibling spec file next to every `.tsx`/`.jsx` source file that
4
+ contains logic (a component, hook, or any function/class with a body).
5
+
6
+ ## Why
7
+
8
+ The shared config already enforces `ddd/require-spec-file`, but the upstream
9
+ [`eslint-plugin-ddd`](https://www.npmjs.com/package/eslint-plugin-ddd) rule only
10
+ inspects `.js`/`.ts` files — it computes its target extension as `.js`/`.ts` and
11
+ bails out on anything else. That silently exempts `.tsx`/`.jsx` files, i.e. the
12
+ React/Preact components this config is primarily built for. The package
13
+ advertises "Requires spec files for **all** source files", yet components — the
14
+ files most worth testing — were never actually covered.
15
+
16
+ `custom/require-spec-file-tsx` closes that gap for the JSX extensions, leaving
17
+ `.js`/`.ts` to the `ddd` plugin so the two halves behave consistently.
18
+
19
+ ## What it checks
20
+
21
+ A `.tsx`/`.jsx` file passes when a sibling `<name>.spec.<ext>` exists in the same
22
+ directory:
23
+
24
+ ```
25
+ components/
26
+ ├── Button.tsx # Requires: Button.spec.tsx
27
+ ├── Button.spec.tsx # ✅ present
28
+ └── index.tsx # excluded (index/barrel file)
29
+ ```
30
+
31
+ As with `ddd/require-spec-file`, only the `.spec.*` name satisfies the
32
+ requirement. A `.test.*` sibling is itself skipped from needing a spec, but it
33
+ does **not** satisfy the requirement for the source file.
34
+
35
+ ## Excluded by default
36
+
37
+ - `index.tsx` / `index.jsx` (barrel files)
38
+ - `*.stories.tsx` / `*.stories.jsx`
39
+ - `.d.ts` declaration files
40
+ - Error/exception files — `*-error.{tsx,jsx}`, `*.error.{tsx,jsx}`, anything
41
+ under `errors/` or `exceptions/` (mirrors the `ddd/require-spec-file`
42
+ exemptions for `.ts`/`.js`)
43
+ - Files with no logic (e.g. pure re-export barrels)
44
+ - `.spec.*` / `.test.*` files themselves
45
+
46
+ ## Options
47
+
48
+ | Option | Type | Default | Description |
49
+ | ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
50
+ | `excludePatterns` | `string[]` | `['**/index.tsx', '**/index.jsx', '**/*.stories.{tsx,jsx}', '**/*.d.ts', '**/*-error.{tsx,jsx}', '**/*.error.{tsx,jsx}', '**/errors/**', '**/exceptions/**']` | Glob patterns to exclude from the spec requirement. |
51
+
52
+ ```js
53
+ 'custom/require-spec-file-tsx': [
54
+ 'error',
55
+ { excludePatterns: ['**/index.tsx', '**/*.stories.{tsx,jsx}'] },
56
+ ]
57
+ ```
@@ -0,0 +1,93 @@
1
+ /* eslint-disable single-export/single-export */
2
+ // Helpers for the require-spec-file-tsx rule.
3
+ //
4
+ // Logic detection and pattern matching mirror eslint-plugin-ddd so the
5
+ // .tsx/.jsx half of the spec-file requirement behaves like the .js/.ts half.
6
+
7
+ export const JSX_EXTENSIONS = ['.tsx', '.jsx']
8
+
9
+ const FUNC_TYPES = new Set([
10
+ 'FunctionDeclaration',
11
+ 'FunctionExpression',
12
+ 'ArrowFunctionExpression',
13
+ ])
14
+
15
+ // Arrow bodies that are a bare value (not a block) and carry no real logic.
16
+ const SIMPLE_BODIES = new Set([
17
+ 'Literal',
18
+ 'Identifier',
19
+ 'ObjectExpression',
20
+ 'ArrayExpression',
21
+ ])
22
+
23
+ // Whether a node introduces real logic that warrants a spec file.
24
+ export const hasLogicInNode = node => {
25
+ if (!node) {
26
+ return false
27
+ }
28
+ if (FUNC_TYPES.has(node.type) && node.body) {
29
+ if (
30
+ node.type === 'ArrowFunctionExpression' &&
31
+ node.body.type !== 'BlockStatement'
32
+ ) {
33
+ return !SIMPLE_BODIES.has(node.body.type)
34
+ }
35
+ return true
36
+ }
37
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
38
+ return node.body.body.some(
39
+ member =>
40
+ member.type === 'MethodDefinition' && member.value && member.value.body
41
+ )
42
+ }
43
+ return false
44
+ }
45
+
46
+ const checkSinglePattern = (filename, pattern) => {
47
+ if (pattern.startsWith('**/') && pattern.endsWith('/**')) {
48
+ return filename.includes(`/${pattern.slice(3, -3)}/`)
49
+ }
50
+ if (pattern.includes('**/')) {
51
+ const suffix = pattern.replace('**/', '')
52
+ if (suffix.startsWith('*')) {
53
+ return filename.endsWith(suffix.slice(1))
54
+ }
55
+ if (suffix.includes('*')) {
56
+ const basename = filename.split('/').pop()
57
+ return suffix.split('*').every(part => basename.includes(part))
58
+ }
59
+ return filename.endsWith(`/${suffix}`)
60
+ }
61
+ return filename.includes(pattern)
62
+ }
63
+
64
+ // Test a filename against glob-ish exclude patterns, supporting the same
65
+ // double-star prefix and brace-expansion forms accepted by the ddd plugin.
66
+ export const checkExcludePatterns = (filename, excludePatterns) => {
67
+ const normalized = filename.replaceAll('\\', '/')
68
+ return excludePatterns.some(pattern => {
69
+ const braceMatch = pattern.match(/\{(?<content>[^}]+)\}/)
70
+ if (braceMatch) {
71
+ return braceMatch.groups.content
72
+ .split(',')
73
+ .some(alt =>
74
+ checkSinglePattern(normalized, pattern.replace(/\{[^}]+\}/, alt))
75
+ )
76
+ }
77
+ return checkSinglePattern(normalized, pattern)
78
+ })
79
+ }
80
+
81
+ export const DEFAULT_EXCLUDE_PATTERNS = [
82
+ '**/index.tsx',
83
+ '**/index.jsx',
84
+ '**/*.stories.{tsx,jsx}',
85
+ '**/*.d.ts',
86
+ // Error/exception files mirror the ddd/require-spec-file exemptions so a
87
+ // React/Preact error component (e.g. an error boundary) is treated the same
88
+ // as its `.ts` counterpart instead of being singled out for a spec file.
89
+ '**/*-error.{tsx,jsx}',
90
+ '**/*.error.{tsx,jsx}',
91
+ '**/errors/**',
92
+ '**/exceptions/**',
93
+ ]