eslint-config-agent 3.0.5 → 3.1.0

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