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.
- package/CHANGELOG.md +158 -0
- package/README.md +592 -26
- package/configs/base-plugins.js +32 -0
- package/configs/config-files.js +19 -3
- package/configs/examples.js +12 -1
- package/configs/javascript.js +12 -4
- package/configs/length-rule-scope.js +27 -0
- package/configs/overrides.js +3 -14
- package/configs/test-files.js +40 -1
- package/configs/tsx.js +10 -0
- package/configs/typescript.js +44 -2
- package/exports/ddd.d.ts +10 -0
- package/exports/ddd.js +1 -3
- package/exports/incremental.d.ts +11 -0
- package/exports/incremental.js +39 -0
- package/exports/recommended-incremental.d.ts +11 -0
- package/exports/recommended-incremental.js +46 -0
- package/exports/recommended.d.ts +14 -0
- package/exports/recommended.js +110 -0
- package/exports/to-warnings.d.ts +20 -0
- package/exports/to-warnings.js +41 -0
- package/index.d.ts +18 -0
- package/index.js +883 -7
- package/package.json +46 -17
- package/plugins/index.js +6 -0
- package/rules/index.js +20 -22
- package/rules/no-inline-union-types/index.js +8 -6
- package/rules/no-process-env-properties/index.js +4 -4
- package/rules/no-record-literal-types/index.js +3 -3
- package/rules/plugin/import/index.js +41 -0
- package/rules/plugin/index.js +0 -2
- package/rules/plugin/n/index.js +16 -2
- package/rules/plugin/n/no-process-env/index.js +4 -4
- package/rules/plugin/react/index.js +150 -2
- package/rules/plugin/typescript-eslint/index.js +406 -0
- package/rules/require-spec-file-tsx/README.md +57 -0
- package/rules/require-spec-file-tsx/helpers.js +93 -0
- package/rules/require-spec-file-tsx/index.js +109 -0
- 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
|
+
]
|