exadev-eslint-config 2.28.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +195 -2
  2. package/dist/index.cjs +1306 -102
  3. package/dist/index.js +1306 -102
  4. package/package.json +2 -1
package/README.md CHANGED
@@ -82,7 +82,6 @@ export default defineConfig(
82
82
  - **[`prefer-readonly`](https://typescript-eslint.io/rules/prefer-readonly/)**, **[`promise-function-async`](https://typescript-eslint.io/rules/promise-function-async/)**, **[`require-array-sort-compare`](https://typescript-eslint.io/rules/require-array-sort-compare/)** — plain presence, no extra config.
83
83
  - **[`strict-void-return`](https://typescript-eslint.io/rules/strict-void-return/)** — bans passing a value-returning function where a void-returning one is expected (e.g. `arr.forEach(x => otherArray.push(x))`).
84
84
  - *Why:* not yet in any typescript-eslint preset; this typechecks today only because of TS's own void-return contravariance leniency.
85
- - **[`switch-exhaustiveness-check`](https://typescript-eslint.io/rules/switch-exhaustiveness-check/)** — plain presence, no extra config.
86
85
  - **[`strict-boolean-expressions`](https://typescript-eslint.io/rules/strict-boolean-expressions/)** at the rule's own bare defaults.
87
86
  - *Why:* an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not.
88
87
  - **[`no-magic-numbers`](https://typescript-eslint.io/rules/no-magic-numbers/)** — tuned to exempt array indexes, enum members, readonly class properties, default parameter values, numeric literal types (e.g. `type Indent = 2 | 4`), and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`).
@@ -92,6 +91,11 @@ export default defineConfig(
92
91
  - *Why:* a deliberately loose backstop against a genuinely excessive number of REQUIRED parameters, distinct from `exadev/prefer-options-object-param` above, which already offers a real fix for the more common shape (a run of 2+ trailing OPTIONAL parameters) this rule structurally cannot see until the total count crosses its own threshold.
93
92
  - **[`no-warning-comments`](https://eslint.org/docs/latest/rules/no-warning-comments)** — bans any comment containing `Stryker disable`.
94
93
  - *Why:* that's Stryker's own mutation-testing suppression directive, invisible to `noInlineConfig` above since it isn't an eslint-disable comment.
94
+ - **[`explicit-module-boundary-types`](https://typescript-eslint.io/rules/explicit-module-boundary-types/)**: an exported function or class member writes out its parameter and return types.
95
+ - *Why:* an inferred return type changes silently when the body does; `isolatedDeclarations` is the compiler-side alternative and is a tsconfig matter.
96
+ - **[`switch-exhaustiveness-check`](https://typescript-eslint.io/rules/switch-exhaustiveness-check/)** set to `{ considerDefaultExhaustiveForUnions: false, requireDefaultForNonUnion: true }`.
97
+ - *Why:* a `default` branch cannot stand in for handling a newly added union member, and a switch over a non-union needs a `default` because the checker cannot prove its cases complete.
98
+ - **Core robustness rules**: `eqeqeq`, `no-implicit-coercion`, `no-param-reassign`, `no-await-in-loop`, `require-atomic-updates`, `default-case-last`, `no-return-assign` and `max-depth`. See [Robustness rules](#robustness-rules).
95
99
 
96
100
  **Test files** (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions, and only these two:
97
101
 
@@ -102,6 +106,30 @@ export default defineConfig(
102
106
 
103
107
  Nothing else inherited from the presets is relaxed.
104
108
 
109
+ ### Robustness rules
110
+
111
+ Core ESLint rules that nothing in the typescript-eslint presets enables. All need no type information, so `plugin.configs.recommended` carries them as well, with the same settings except `no-implicit-coercion` (below).
112
+
113
+ | Rule | Setting | Why |
114
+ | --- | --- | --- |
115
+ | [`eqeqeq`](https://eslint.org/docs/latest/rules/eqeqeq) | `always` | `== null` hides which of `null` and `undefined` the code means, so there is no `null` carve-out. |
116
+ | [`no-implicit-coercion`](https://eslint.org/docs/latest/rules/no-implicit-coercion) | `{ boolean: false }` in the default export, defaults in `plugin.configs.recommended` | `!!value` is allowed where `strict-boolean-expressions` backs it, because that rule already rejects `!` on an operand whose truthiness is ambiguous (a nullable string or number). `plugin.configs.recommended` registers no typescript-eslint rules, so it keeps banning `!!`. `+value`, `'' + value` and similar stay banned in both. |
117
+ | [`no-param-reassign`](https://eslint.org/docs/latest/rules/no-param-reassign) | `{ props: true }` | A parameter is the caller's value; assigning to it or to one of its properties is a hidden side effect. |
118
+ | [`no-await-in-loop`](https://eslint.org/docs/latest/rules/no-await-in-loop) | on | Iterations that could run together run one after another. `for await` is allowed. |
119
+ | [`require-atomic-updates`](https://eslint.org/docs/latest/rules/require-atomic-updates) | on | `total += await p` reads `total` before the `await` and writes it after, losing any update made in between. |
120
+ | [`default-case-last`](https://eslint.org/docs/latest/rules/default-case-last) | on | A `default` that is not last hides the cases written after it. |
121
+ | [`no-return-assign`](https://eslint.org/docs/latest/rules/no-return-assign) | `always` | Even a parenthesised assignment cannot be the returned value. |
122
+ | [`max-depth`](https://eslint.org/docs/latest/rules/max-depth) | `{ max: 4 }` | The rule's own default, written out so a change to it cannot loosen the limit. |
123
+
124
+ A loop that is sequential by design (migrations that must run in order, a rate-limited API) is exempted by a `files`-scoped override placed after the shared config, never by an inline disable comment, since `noInlineConfig` is on:
125
+
126
+ ```ts
127
+ export default defineConfig(...exadevConfig(), {
128
+ files: ['src/migrations/**/*.ts'],
129
+ rules: { 'no-await-in-loop': 'off' },
130
+ });
131
+ ```
132
+
105
133
  ## The lighter option: the `plugin` named export
106
134
 
107
135
  For a project that wants only this package's own rules without the full type-checked bundle, import the named `plugin` export and wire rules individually:
@@ -134,7 +162,7 @@ export default defineConfig([
134
162
  {
135
163
  files: ['**/*.ts'],
136
164
  plugins: { exadev: plugin },
137
- extends: ['exadev/recommended'], // this plugin's own non-type-aware rules, plus linterOptions.noInlineConfig — no type-checked rules at all
165
+ extends: ['exadev/recommended'], // this plugin's own non-type-aware rules, the core robustness rules, plus linterOptions.noInlineConfig — no type-checked rules at all
138
166
  // or: extends: ['exadev/barrel'], // just the barrel-discipline trio (no-non-barrel-index, no-non-barrel-reexport, no-side-effects-in-index)
139
167
  },
140
168
  ]);
@@ -370,10 +398,17 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
370
398
  | `filename-pattern` | | **Filename conventions by glob:** a name regex, a required sibling file, and a naming scheme required once a file passes a line count. See [Filename patterns](#filename-patterns). |
371
399
  | `pure-module` | | **Bans I/O, ambient state and `async` in the files it is wired onto:** imports of Node I/O modules, I/O and scheduling globals, argument-less `Date`, `Date.now`, `Math.random`, and `async`/`await`. Wired by `pureModulesConfig`. See [Pure modules](#pure-modules). |
372
400
  | `scoped-first-parameter` | | **Every method of the configured repository-like interfaces takes a scope parameter first.** Checks the signature only, not that the scope is used or that tenants are isolated. Requires type information. Opt-in: needs its `interfaces` and `parameter` options. See [Scoped first parameter](#scoped-first-parameter). |
401
+ | `require-compiler-options` | | **The effective compiler options of the tsconfig ESLint type-checks against must have the configured values.** `extends` is resolved the way `tsc` resolves it. Reports once per tsconfig and run, and names the tsconfig it resolved. Requires type information. Opt-in. See [Compiler options](#compiler-options). |
402
+ | `vitest-config` | | **A Vitest config must not stop the run from failing:** `passWithNoTests: true`, `allowOnly: true`, a bare `node_modules` exclude, and coverage thresholds under a key Vitest reads as a file glob. Self-scopes to `vitest*.config.*` and `vitest.workspace.*`. Opt-in. See [Tool config files](#tool-config-files). |
403
+ | `vitest-coverage-config` | | **The shared base Vitest config must spell out a complete `coverage` block:** `provider`, `include` and all four `thresholds` keys. A no-op unless `files` names the base config files. See [Tool config files](#tool-config-files). |
404
+ | `stryker-break-threshold` | | **A Stryker config must set `thresholds.break` to a number,** optionally no lower than a minimum and no lower than the value in a shared base config. Self-scopes to `stryker*.config.*`. See [Tool config files](#tool-config-files). |
405
+ | `stryker-thresholds-order` | | **Stryker `thresholds` must satisfy `break <= low <= high`,** using Stryker's defaults for a key the config leaves out. See [Tool config files](#tool-config-files). |
406
+ | `playwright-config` | | **A Playwright config must set `forbidOnly`** to `true` or a non-literal expression such as `!!process.env.CI`, with optional checks that `fullyParallel` and `workers` are set. Self-scopes to `playwright*.config.*`. See [Tool config files](#tool-config-files). |
373
407
  | `injected-test-hygiene` | | **A conformance kit's injected test functions get the same hygiene as imported ones:** no `.only`, no `.skip`, and an assertion in every test body, for the `describe` and `it` a kit receives as parameters, which `@vitest/eslint-plugin` skips. Wired by `testHygieneConfig`. See [Guard and conformance test hygiene](#guard-and-conformance-test-hygiene). |
374
408
  | `non-vacuous-guard` | | **A guard test must show it can fail:** an unconditional lower bound on what it discovered, and its pattern checked against an input it must catch and one it must not. Wired by `testHygieneConfig`. See [Guard and conformance test hygiene](#guard-and-conformance-test-hygiene). |
375
409
  | `no-multiline-template-literal` | ✓ | **An untagged template literal whose value spans several lines should be an array of lines joined with `\n`.** Autofixes only when the rewrite provably yields the same string. See [Multi-line template literals](#multi-line-template-literals). |
376
410
  | `markdown-required-heading` | | **A Markdown document must contain each configured heading:** a `{ depth, text }` per required heading, matched on the text as it renders. A Markdown-language rule (`@eslint/markdown`). Wired by `markdownHeadingsConfig`. See [Required Markdown headings](#required-markdown-headings). |
411
+ | `no-defensive-fallback` | | **An empty-literal fallback hides a value that should have been modelled as absent.** Reports `value ?? []`, `value || ''` and the logical-assignment forms for an empty array, object or string, `0`, `false` or `null`, and a `catch` clause or `.catch()` handler that discards the error and returns nothing or a fixed value. Opt-in, not part of `recommended` or the default export. Needs no type information. See [Defensive fallbacks](#defensive-fallbacks). |
377
412
  | `timeout-aborts-request` | | **A `Promise.race` timeout must abort the request it raced against,** not only settle the race: the timer callback calls `.abort()` on an `AbortController` created in the same function, the timer id is cleared in a `finally`, and a `catch` returns early on `controller.signal.aborted`. Opt-in, needs no type information. See [Timeout races](#timeout-races). |
378
413
  | `no-non-serialisable-server-prop` | | **A configured prop (default `component`) of a JSX element in a file without `"use client"` must be serialisable data.** A function or component reference cannot cross from a server component to a client component. Enabled by the Next.js preset. See [Server component boundary](#server-component-boundary). |
379
414
  | `no-external-member-jsx-tag` | | **A member tag (`<Lib.Icon>`) rooted at an import from a package, in a file without `"use client"`, is reported.** Statics a library attaches after export may not survive the server component boundary. Enabled by the Next.js preset. See [Server component boundary](#server-component-boundary). |
@@ -735,6 +770,159 @@ The rule is syntactic and reads one function. It cannot see that a helper the ra
735
770
 
736
771
  Off unless enabled (`'exadev/timeout-aborts-request': 'error'` with the plugin registered), and not in `recommended`, since a project chooses to adopt the shape.
737
772
 
773
+ ## Defensive fallbacks
774
+
775
+ A fallback on a value that really can be absent turns the absence into a plausible answer, and the wrong result surfaces far from the cause. `@typescript-eslint/no-unnecessary-condition` reports only the fallbacks the types already prove unnecessary; `exadev/no-defensive-fallback` reports the ones they do not, by shape alone:
776
+
777
+ - a `??` or `||` (or `??=` / `||=`) whose right operand is an empty array, empty object, empty string (`''`, `""` or an empty template), `0`, `0n`, `false` or `null`, including one behind `as`, `satisfies` or `<T>`;
778
+ - a `catch` clause whose body is empty (a comment-only body included), or holds one `continue` or one `return` that gives back nothing or a fixed value: a literal, `undefined`, `void 0`, or an array or object built only from those;
779
+ - a `.catch(handler)` or `.then(onFulfilled, handler)` whose rejection handler does the same, as an expression-bodied arrow or a function body.
780
+
781
+ A catch that does anything else with the error (logs it, rethrows it, returns something computed from it) is not reported. The fix is to model absence as `T | undefined` and handle it where it is meaningful, or to let the failure reach the caller.
782
+
783
+ It is opt-in: it is in neither `plugin.configs.recommended` nor the default export, because a configuration reader legitimately treats a missing optional field as empty, and that boundary differs per repository. `allow` lists the files where the fallback is at such a boundary, each with a `reason` (required, so the exemption explains itself where it is configured, since `noInlineConfig` leaves no comment to do so). `files` follows the [file glob dialect](#file-level-rules), so a glob without a `/` names a file at any depth.
784
+
785
+ Two shapes are reported although they are sometimes right, and the fix for both is an `allow` entry. A predicate that turns a failure into `false` (`try { JSON.parse(text); return true; } catch { return false; }`) is reported as a catch that returns a fixed value. `.catch` is matched on any receiver, so a schema fallback such as Zod's `z.string().catch('')` is reported as a promise handler when it is written as a function returning a fixed value.
786
+
787
+ ```ts
788
+ export default defineConfig(...exadevConfig(), {
789
+ files: ['src/**/*.ts'],
790
+ plugins: { exadev: plugin },
791
+ rules: {
792
+ 'exadev/no-defensive-fallback': [
793
+ 'error',
794
+ { allow: [{ files: ['src/config/**'], reason: 'optional settings default to empty at the parsing boundary' }] },
795
+ ],
796
+ },
797
+ });
798
+ ```
799
+
800
+ Overlap: core `no-empty` already reports a catch block with no statements and no comment; this rule also reports the comment-only block, which `no-empty` accepts.
801
+
802
+ ## Compiler options
803
+
804
+ Several enforcement patterns hold only while the compiler is strict: an exhaustive `satisfies Record<Command['type'], Handler>` map, `noUncheckedIndexedAccess` narrowing, `exactOptionalPropertyTypes`. Turning `strict` off in a tsconfig silently disables all of them, and nothing in the config notices. `exadev/require-compiler-options` reports it.
805
+
806
+ ```ts
807
+ // eslint.config.ts
808
+ import { defineConfig } from 'eslint/config';
809
+ import { exadevConfig } from '@exadev/eslint-config';
810
+
811
+ export default defineConfig(
812
+ ...exadevConfig(),
813
+ {
814
+ files: ['**/*.ts'],
815
+ rules: {
816
+ 'exadev/require-compiler-options': [
817
+ 'error',
818
+ {
819
+ strict: true,
820
+ noUncheckedIndexedAccess: true,
821
+ exactOptionalPropertyTypes: true,
822
+ noImplicitReturns: true,
823
+ noImplicitOverride: true,
824
+ noFallthroughCasesInSwitch: true,
825
+ noPropertyAccessFromIndexSignature: true,
826
+ noUncheckedSideEffectImports: true,
827
+ noUnusedLocals: true,
828
+ noUnusedParameters: true,
829
+ verbatimModuleSyntax: true,
830
+ isolatedModules: true,
831
+ strictBuiltinIteratorReturn: true,
832
+ forceConsistentCasingInFileNames: true,
833
+ allowUnreachableCode: false,
834
+ allowUnusedLabels: false,
835
+ target: ['es2022', 'es2023', 'es2024', 'esnext'],
836
+ },
837
+ ],
838
+ },
839
+ },
840
+ );
841
+ ```
842
+
843
+ Each key is a compiler option and each value says what the effective option must be: `true`, `false`, or a list of accepted values. An enum-valued option such as `target`, `module` or `moduleResolution` takes its tsconfig spelling, case-insensitively, and the rule converts it with the compiler's own tsconfig parser, so a list of accepted values is also how "at least" is written. An unknown option, a value the compiler rejects and a list-valued option such as `lib` throw when the rule is created, quoting the compiler.
844
+
845
+ The rule asks the program ESLint already has which tsconfig governs the file (`program.getCompilerOptions().configFilePath`, so `parserOptions.project` and `projectService` both work) and reads that configuration through TypeScript's own config parser, which follows the whole `extends` chain exactly as `tsc` does. A flag inherited from a shared base counts, and a flag the base sets but a package overrides does not. It re-reads the tsconfig rather than trusting `program.getCompilerOptions()` because typescript-eslint overrides `noEmit`, `noUnusedLocals`, `noUnusedParameters`, `allowJs` and `checkJs` in the programs it builds, so those options would look satisfied whatever the tsconfig says. A program with no tsconfig behind it, and none attributed to it as described below, has nothing to re-read and is judged on its own options, in which those five are the overridden values.
846
+
847
+ The value compared is the one the compiler acts on: a flag that follows `strict` (`strictNullChecks`, `noImplicitAny`) is judged on its effective value, and an option that is unset takes the plain default the installed compiler documents for it. An option whose unset state is neither true nor false, such as `allowUnreachableCode`, is reported as not set even when `false` is required, because `false` is a different setting from leaving it out. This reads the compiler's own option metadata, which TypeScript exports at run time without publishing in its typings, and needs a release that has it; the rule throws a clear error naming the requirement otherwise.
848
+
849
+ A tsconfig governs every file of its program, so the rule reports once per tsconfig and rule configuration per run, on the first file ESLint lints under it, and not once per source file. ESLint gives a rule no signal that a run has started, so the rule reads it from the order of the files it lints: a file it has already linted since the last report starts a new run, which a long-lived process (an editor integration, an ESLint instance that lints more than once) meets when it lints that file again in its next run, so the finding is reported again while the tsconfig still violates the requirement. A file linted again straight after itself with different text is ESLint applying fixes, which re-lints the fixed text and keeps only the last pass's messages, or an editor re-linting an edit; that lint repeats the file's previous outcome, so a run that applies fixes still reports once, on the same file. A run that lints only files not linted since the last report cannot be told apart from the same run and reports nothing.
850
+
851
+ The rule keeps this per tsconfig and not per TypeScript program, because typescript-eslint replaces programs. It rebuilds a watch or project service program after an edit, and when it infers a single run (`TSESTREE_SINGLE_RUN=true`, or by default `CI=true` or the `eslint` binary, either without `--fix`) it builds one program per tsconfig ahead of time and parses a file it has already parsed in the process, as in a fix pass through ESLint's API, with a program of that one file and no tsconfig. Such a program is attributed to the tsconfig the file was last linted under, as long as that tsconfig still lists the file, so the file is judged against that tsconfig and not against the throwaway program's defaults.
852
+
853
+ The message names the tsconfig it resolved and lists each option whose effective value differs together with the value required. `parserOptions.project` or `projectService` may select a different tsconfig from the one the build uses (a `tsconfig.eslint.json`, say), in which case the rule checks the lint tsconfig and says nothing about the build; the two need to agree for the check to mean anything. A root tsconfig that covers only tooling files, with each package carrying its own, makes the same point: the tsconfig the rule sees is the one that contains the file being linted.
854
+
855
+ Keep the strictness flags in a base config that compiles nothing and have the root and every package extend it. `skipLibCheck` is common in shared bases and weakens the checking of declaration files, so requiring it `false` is possible (`skipLibCheck: false`) but belongs in a repository that wants it, not in a default set. `isolatedDeclarations` is the compiler-side way to require explicit types on exported declarations and can be added to the set as `isolatedDeclarations: true`; `@typescript-eslint/explicit-module-boundary-types` in the default export is the lint-side alternative.
856
+
857
+ The rule needs type information, so it is in neither `plugin.configs.recommended` nor the default export. ESLint's `--cache` keys on a file's content and the ESLint configuration, not on tsconfig contents, so clear the cache after changing a tsconfig or the option set.
858
+
859
+ ## Tool config files
860
+
861
+ Tool configs decide whether the tools they configure enforce anything, and several can be set so that they enforce nothing: a test runner that passes when it finds no tests, `allowOnly` left on so a focused test reaches CI, coverage with no thresholds, a mutation run with no `break` threshold. These rules lint the config files as ordinary TypeScript. They read the object the file exports and run no tool.
862
+
863
+ Each rule scopes itself by filename, so it can be switched on for every JavaScript and TypeScript file and acts only on the files it knows:
864
+
865
+ | Rule | Built-in scope |
866
+ | --- | --- |
867
+ | `vitest-config` | `vitest*.config.*` and `vitest.workspace.*`, so `vitest.mutation.config.ts` is covered |
868
+ | `vitest-coverage-config` | none: only the files named in its `files` option |
869
+ | `stryker-break-threshold`, `stryker-thresholds-order` | `stryker*.config.*`, `stryker*.conf.*` and the dot-prefixed `.stryker.config.*` and `.stryker.conf.*` |
870
+ | `playwright-config` | `playwright*.config.*` |
871
+
872
+ Every rule takes a `files` option, a glob or list of globs in the [file glob dialect](#file-level-rules), that replaces the built-in scope; a glob without a `/` names a file at any depth.
873
+
874
+ **Only what the file spells out is judged.** A rule follows the default export (`export default`, `export =` or `module.exports =`) through an object literal, an array, a call to a config helper (`defineConfig`, `defineProject`, `defineWorkspace`, `mergeConfig`), the returned value of a function, either branch of a conditional, and a top-level `const`. A value that is the result of any other call, an import, a spread or a computed key is not visible, and the rule stays silent about it instead of guessing. A package config that merges a shared base and sets only a few keys is therefore judged only on those keys, which is why the checks that need a complete picture run on the base file itself (`vitest-coverage-config`, and the `base` option of `stryker-break-threshold`).
875
+
876
+ ```ts
877
+ // eslint.config.ts
878
+ import { defineConfig } from 'eslint/config';
879
+ import { exadevConfig } from '@exadev/eslint-config';
880
+
881
+ export default defineConfig(
882
+ ...exadevConfig(),
883
+ {
884
+ files: ['**/*.config.{ts,mts,cts,js,mjs,cjs}'],
885
+ rules: {
886
+ 'exadev/vitest-config': 'error',
887
+ 'exadev/vitest-coverage-config': ['error', { files: ['vitest.base.config.ts'] }],
888
+ 'exadev/stryker-break-threshold': ['error', { min: 80, base: { path: 'stryker.base.config.ts', relativeTo: 'root' } }],
889
+ 'exadev/stryker-thresholds-order': 'error',
890
+ 'exadev/playwright-config': 'error',
891
+ },
892
+ },
893
+ );
894
+ ```
895
+
896
+ ### Vitest
897
+
898
+ `exadev/vitest-config` reports, in the `test` object of a config and of every inline project in `test.projects` (at any depth) and in the project objects of a `vitest.workspace.*` file:
899
+
900
+ - `passWithNoTests: true`, which makes the run pass when no test file is found, so a mistyped include glob or a deleted suite goes unnoticed;
901
+ - `allowOnly: true`, which lets a committed `.only` through (Vitest's own default already allows it only outside CI);
902
+ - the bare string `node_modules` (or `node_modules/`) in `test.exclude`, which excludes only a top-level directory of that name; `**/node_modules/**` is the form that also excludes nested copies. `test.coverage.exclude` is not checked: coverage filtering matches with `contains: true`, so a bare name there also ignores nested copies;
903
+ - a key under `test.coverage.thresholds` that Vitest does not read. Verified against the installed Vitest source, which skips `statements`, `branches`, `functions`, `lines`, `perFile`, `autoUpdate` and `100` and treats every other key as a file glob matched against the covered files, so thresholds nested under a word such as `global` match no file and enforce nothing. A key counts as intended for a glob or a file when it contains a path separator, a dot (so a root file name such as `index.ts` is accepted) or one of `*`, `?`, `[`, `]`, `{`, `}`, `!`; anything else is reported. This was checked against Vitest 4 only.
904
+
905
+ The option `forbidNumericMaxWorkers: true` also reports a numeric `test.maxWorkers`, for a repository whose task runner already parallelises across packages and where a fixed worker count in each package multiplies. A percentage string or a computed value passes. The same setting in a `package.json` script is the concern of [`required-scripts`](#required-scripts): `{ name: 'test', excludes: ['--passWithNoTests'] }` reports the flag on the command line.
906
+
907
+ `exadev/vitest-coverage-config` takes `files`, the shared base config files, and does nothing without it. In those files, every `test.coverage` block that is an object literal must set `provider`, `include` and a `thresholds` object with `statements`, `branches`, `functions` and `lines`, and one diagnostic lists everything missing. Per-package configs merge the base with `mergeConfig` and often set only `coverage.exclude`, so requiring thresholds in every file would report each of them for what it inherits.
908
+
909
+ ### Stryker
910
+
911
+ `exadev/stryker-break-threshold` requires `thresholds.break` to be a number, since a run with no `break` (the default is `null`) never fails however low the mutation score falls. A missing `thresholds`, a missing `break`, `null`, `undefined` and a non-number are reported. A mutation job may deliberately run without `break` until a real threshold has been measured, so the rule is its own rule: set it to `'warn'`, or switch it off for those files, without touching the ordering check.
912
+
913
+ A `break` is a gate only when it is high enough to fail something, and a value of 2 never does. Two options set a floor:
914
+
915
+ - `min`, a number from 0 to 100;
916
+ - `base`, a file reference (`{ path, relativeTo? }`, resolved against the linted file's directory or the workspace root) to the shared Stryker config. The rule parses that file, takes its `thresholds.break`, and reports a package whose own value is lower, so a package cannot quietly undercut the base. It throws, naming the file, when the base does not exist or spells out no numeric `break`, rather than comparing against nothing.
917
+
918
+ With both, the higher floor is the one named in the message.
919
+
920
+ `exadev/stryker-thresholds-order` requires `break <= low <= high`. A key left out takes Stryker's documented default (`high` 80, `low` 60) and `break` has none, so `thresholds: { break: 70 }` is reported because it exceeds the default `low`: the low and high marks are then unreachable on a passing run, which usually means they were left at their defaults when the break was raised. Equal marks are allowed, since `{ high: 100, low: 100, break: 100 }` is the strictest valid configuration. Stryker itself rejects only a `low` above `high`.
921
+
922
+ ### Playwright
923
+
924
+ `exadev/playwright-config` requires `forbidOnly` to be set, because Playwright defaults it to false and a committed `test.only` then runs one test and passes. `true` and any non-literal expression pass, so `forbidOnly: !!process.env.CI` is accepted; an absent key, `false`, `undefined` and `null` are reported. Two options add checks that a setting is present: `fullyParallel: true` requires `fullyParallel` to be set and not false, and `workers: true` requires `workers` to be set to anything, so the worker count on CI and locally is a stated choice. Only the top level of the config is read.
925
+
738
926
  ## Workspace architecture
739
927
 
740
928
  Six rules (`no-uphill-dependency`, `no-dependency-cycle`, `package-name-mirrors-path`, `package-has-files`, `dev-dependency-only`, `required-scripts`) share one options object, `WorkspaceArchitectureOptions`, describing a pnpm workspace's own dependency-direction rules (which package is allowed to depend on which other, checked directly against every `package.json`'s declared `dependencies`) and the per-package requirements layered on the same groups. The last four rules are opt-in within it: each does nothing unless its own option is given. Off by default, since `groups` has no sensible default; enable it either through `exadevConfig({ workspaceArchitecture })` (the full bundle) or the standalone `workspaceArchitectureConfig(options)` export (for a consumer building its own config from the lighter `plugin` export, e.g. one that isn't using `exadevConfig()` at all):
@@ -1129,9 +1317,14 @@ pnpm build
1129
1317
  - [`src/turbo-config.ts`](src/turbo-config.ts) builds the turbo blocks (`buildTurboConfig` internally, `turboConfig` publicly) from the one shared options object in [`src/rules/turbo-options.ts`](src/rules/turbo-options.ts).
1130
1318
  - The pure decisions live in [`src/rules/turbo-checks.ts`](src/rules/turbo-checks.ts), independent of ESLint, so they are tested against plain maps; the rules are thin visitors over them. [`src/rules/turbo-json.ts`](src/rules/turbo-json.ts) reads the parts of a `turbo.json` the rules need and finds the root one, [`src/rules/turbo-workspace.ts`](src/rules/turbo-workspace.ts) lists the packages and their scripts, and [`src/rules/turbo-commands.ts`](src/rules/turbo-commands.ts) reads a script command for a delegation, the tasks it invokes, or a boundaries run.
1131
1319
  - Rules that read sibling and ancestor files take a `WorkspaceFs` through their factory, and their tests use [`src/rules/memory-fs.ts`](src/rules/memory-fs.ts), an in-memory implementation, rather than fixture trees on disk.
1320
+ - [`src/rules/static-config.ts`](src/rules/static-config.ts) is the shared reader for the tool config rules. `createStaticConfig(program)` answers only what a config file's source states: which object literals the default export reaches, whether a key is `present`, `absent` or `opaque` (a later spread or non-literal computed key could supply it), and the literal a value stands for. [`src/rules/tool-config-options.ts`](src/rules/tool-config-options.ts) holds the option handling they share, and [`src/rules/vitest-test-objects.ts`](src/rules/vitest-test-objects.ts) finds the `test` objects of a Vitest config, projects included. `stryker-break-threshold` parses its `base` file with `@typescript-eslint/typescript-estree` and reads it through the same `createStaticConfig`.
1321
+ - [`src/rules/compiler-option-values.ts`](src/rules/compiler-option-values.ts) is where `require-compiler-options` agrees with `tsc`. `tsconfigPathOf` finds the tsconfig a program was created from, and `readTsconfig` re-reads it, with the files it lists, because typescript-eslint overrides some options in its programs. TypeScript exports `computedOptions` (how it derives an effective option from the others) and `optionDeclarations` (every option with its documented default) at run time but not in its typings; this module reads them through `Reflect.get` and narrows with guards, converts a required value with the compiler's own `convertCompilerOptionsFromJson`, and throws when the installed compiler does not export them.
1322
+ - [`src/rules/run-tracker.ts`](src/rules/run-tracker.ts) decides which lint carries a finding that belongs to a set of files rather than to one, so `require-compiler-options` reports once per run; it reads run boundaries from the order of the lints, as [Compiler options](#compiler-options) describes.
1323
+ - [`src/rules/tsconfig-attribution.ts`](src/rules/tsconfig-attribution.ts) works out which tsconfig governs a linted file, attributing a program with no tsconfig to the one the file was last linted under while that tsconfig still lists it.
1132
1324
  - [`src/create-config.ts`](src/create-config.ts) is config assembly's single source of truth.
1133
1325
  - `exadevConfig(options, ...userConfigs)` concatenates, in order: `buildGitignoreConfig`, `recommendedTypeChecked`, `jsdocAndTsdoc`, `jsonCanonicalConfig`, `stylisticCommentsConfig`, `buildReactConfig`, `buildNextjsConfig`, `buildPackageJsonKeyOrderConfig` (each tri-state builder fed its matching option), `buildWorkspaceArchitectureConfig` (only when `workspaceArchitecture` is given; there is no auto-detected default), `buildTurboConfig` (only when `turbo` is given, likewise), `buildImportPolicyConfig` (only when `importPolicies` is given, likewise), `buildPureModulesConfig` (only when `pureModules` is given, likewise), `buildTestHygieneConfig` (only when `testHygiene` is given, likewise), `buildMarkdownHeadingsConfig` (only when `markdownHeadings` is given, likewise), and any trailing user configs.
1134
1326
  - `defaultConfig` is `exadevConfig()` evaluated once, eagerly, at module load.
1327
+ - [`src/robustness-rules.ts`](src/robustness-rules.ts) holds `UNTYPED_ROBUSTNESS_RULES`, the core ESLint rules that need no type information, spread into both `plugin.configs.recommended` and the type-checked block so the two cannot drift apart.
1135
1328
  - [`src/stylistic-comments.ts`](src/stylistic-comments.ts) builds `stylisticCommentsConfig`: the hand-picked `@stylistic/eslint-plugin` rules plus this package's own `exadev/prefer-doc-comment`, in two blocks (one scoped to every JS/TS file, one scoped to JSX files only for the three JSX-specific rules). See [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules).
1136
1329
  - [`src/index.ts`](src/index.ts) is the entry point, still a pure re-export barrel:
1137
1330
  ```ts