exadev-eslint-config 2.36.0 → 2.36.2

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/README.md CHANGED
@@ -94,12 +94,14 @@ export default defineConfig(
94
94
  - *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.
95
95
  - **[`no-warning-comments`](https://eslint.org/docs/latest/rules/no-warning-comments)** — bans any comment containing `Stryker disable`.
96
96
  - *Why:* that's Stryker's own mutation-testing suppression directive, invisible to `noInlineConfig` above since it isn't an eslint-disable comment.
97
- - **[`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.
97
+ - **[`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. TypeScript files only (`.ts`, `.tsx`, `.mts`, `.cts`): the rule reads annotations from the syntax, never from JSDoc, so it could not be satisfied in plain JavaScript typed through `@type`, `@param {T}` and `@returns {T}`; `tsc --checkJs` covers those files.
98
98
  - *Why:* an inferred return type changes silently when the body does; `isolatedDeclarations` is the compiler-side alternative and is a tsconfig matter.
99
99
  - **[`switch-exhaustiveness-check`](https://typescript-eslint.io/rules/switch-exhaustiveness-check/)** set to `{ considerDefaultExhaustiveForUnions: false, requireDefaultForNonUnion: true }`.
100
100
  - *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.
101
101
  - **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).
102
102
 
103
+ **JSDoc and TSDoc** (see [`src/jsdoc.ts`](src/jsdoc.ts)): [`eslint-plugin-jsdoc`](https://github.com/gajus/eslint-plugin-jsdoc)'s `flat/recommended-tsdoc-error` bundle checks an existing doc comment in every JavaScript and TypeScript file, with the requirements tier (whether a comment must exist) turned off, and [`eslint-plugin-tsdoc`](https://github.com/microsoft/tsdoc/tree/main/eslint-plugin)'s `tsdoc/syntax` validates its TSDoc syntax. Two rules apply to TypeScript files only (`.ts`, `.tsx`, `.mts`, `.cts`): `tsdoc/syntax` and `jsdoc/no-types`, which together forbid a `{type}` in a doc comment because TypeScript already states the type. In plain JavaScript (`.js`, `.jsx`, `.mjs`, `.cjs`) a JSDoc annotation (`@type {...}`, `@param {T}`, `@returns {T}`) is the only place a type can be written, and `tsc --checkJs` reads it, so both rules stay off there and the rest of the jsdoc bundle (tag names, parameter names, type validity) still applies.
104
+
103
105
  **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:
104
106
 
105
107
  - **`@ts-expect-error`** reverts to `allow-with-description`.
@@ -339,7 +341,7 @@ The plugin's own full-canonicalization rule ([`no-insignificant-whitespace`](htt
339
341
 
340
342
  A hand-picked subset of [`@stylistic/eslint-plugin`](https://eslint.style) is bundled unconditionally, the same way jsdoc/tsdoc and RFC 8785 JSON support are: `@stylistic/eslint-plugin` is a plain dependency of this package, so every consumer already has it, and every rule below matches purely on comment shape, statement/class-member placement, or JSX prop/tag conventions, never on formatting (indentation, quotes, semicolons, trailing commas, line wrapping, bracket spacing), which stays Prettier's job in every consumer of this package. None of these rules appears in [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier)'s own disabled-rules list.
341
343
 
342
- - **[`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style)** at `'bare-block'`, through this package's own wrapper `exadev/multiline-comment-style` rather than the plugin's rule id: a run of `//` lines becomes one `/* ... */` block. The wrapper exists because upstream does not yet treat `TODO`/`FIXME` lines as directives ([eslint-stylistic/eslint-stylistic#1287](https://github.com/eslint-stylistic/eslint-stylistic/issues/1287)), nor `cspell:*`, `biome-ignore` or `#region`/`#endregion`, and its fixer folds them into the merged block. Upstream already keeps its own directives (`prettier-ignore`, `c8 ignore`/`v8 ignore`, `istanbul ignore`, `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, triple-slash reference/AMD directives and the rest of its list) as standalone lines since [PR #1251](https://github.com/eslint-stylistic/eslint-stylistic/pull/1251). A folded marker is not cosmetic: `exadev/prefer-doc-comment` leaves a block containing a directive alone, so an exported declaration whose leading run held a `TODO` would silently lose its doc comment. The wrapper runs the upstream rule unchanged (options, defaults, messages and fixer are upstream's), except that every line `exadev/prefer-doc-comment` treats as a directive is hidden from its grouping, so such a line breaks a run exactly as an upstream directive does and the prose on each side is converted on its own. The upstream fixer writes a bare `\n` inside the merged block in a CRLF file, and the wrapper does not change that.
344
+ - **[`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style)** at `'bare-block'`, through this package's own wrapper `exadev/multiline-comment-style` rather than the plugin's rule id: a run of `//` lines becomes one `/* ... */` block. The wrapper exists because upstream does not yet treat `TODO`/`FIXME` lines as directives ([eslint-stylistic/eslint-stylistic#1287](https://github.com/eslint-stylistic/eslint-stylistic/issues/1287)), nor `cspell:*`, `biome-ignore` or `#region`/`#endregion`, and its fixer folds them into the merged block. Upstream already keeps its own directives (`prettier-ignore`, `c8 ignore`/`v8 ignore`, `istanbul ignore`, `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, triple-slash reference/AMD directives and the rest of its list) as standalone lines since [PR #1251](https://github.com/eslint-stylistic/eslint-stylistic/pull/1251). A folded marker is not cosmetic: `exadev/prefer-doc-comment` leaves a block containing a directive alone, so an exported declaration whose leading run held a `TODO` would silently lose its doc comment. The wrapper runs the upstream rule unchanged (options, defaults, messages and fixer are upstream's), except that every line `exadev/prefer-doc-comment` treats as a directive is hidden from its grouping, so such a line breaks a run exactly as an upstream directive does and the prose on each side is converted on its own. The upstream fixer writes a bare `\n` inside the merged block in a CRLF file, and the wrapper does not change that. The wrapper strips trailing whitespace from the text the upstream fixer writes (upstream leaves it on blank lines inside a rewritten block), and a managed-region marker (a comment of exactly `<word>:<word>:start` or `<word>:<word>:end`, such as `content:claude:start`) is treated as a directive, since the tool that owns it finds it by its exact line.
343
345
  - **[`spaced-comment`](https://eslint.style/rules/spaced-comment)**: requires a space after `//`/`/*`, without fighting a `/**`-opening doc comment or a shebang line. Adds `{ block: { markers: ['!'] } }` on top of the bare default so a `/*!` license/banner block is not flagged either. A triple-slash reference/AMD directive is exempted by the rule itself in both its spaced form (`/// <reference types="..." />`, TypeScript's own compiler-emitted spelling) and its no-space form (`///<reference types="..." />`, also valid TypeScript syntax); before `@stylistic/eslint-plugin` 6.0.0, `--fix` rewrote the no-space form into a broken `// /<reference ...>` comment ([ExaDev/eslint-config#47](https://github.com/ExaDev/eslint-config/issues/47)). Also adds `{ line: { markers: ['#', '#region', '#endregion'] } }`: without it, the bare default rewrites a `//# sourceMappingURL=...`/`//# sourceURL=...` source-map comment into `// # sourceMappingURL=...`, which browsers and Node no longer recognise, and a bare `line: { markers: ['#'] }` alone would instead rewrite an unspaced `//#region`/`//#endregion` editor fold marker into `//# region`, which VS Code's own folding regex no longer matches. Listing all three markers together, confirmed directly against the installed rule, leaves every one of those four shapes untouched.
344
346
  - **[`lines-between-class-members`](https://eslint.style/rules/lines-between-class-members)** at its bare default: a blank line between class members, except after a `.d.ts`-style method overload signature.
345
347
  - **[`line-comment-position`](https://eslint.style/rules/line-comment-position)** at `'above'`: a `//` comment belongs above the code it describes, never trailing beside it on the same line. Not autofixable; a flagged trailing comment needs moving by hand. A trailing `// cspell:disable-line` is exempt (`ignorePattern`): cspell honours that directive only on the line it suppresses, so moving it above, as the report demands, would silently disable spell-checking for the wrong line. Every other directive this package recognises applies to the line(s) below it and is correctly relocated.
@@ -389,8 +391,8 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
389
391
  | `no-enum-reverse-lookup-widening` | suggestion | **A numeric enum's reverse lookup can silently type as `string` for an out-of-range index.** Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime — `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
390
392
  | `prefer-numeric-sort-compare` | suggestion | **`.sort()` on a number array sorts lexicographically by default.** A deliberately narrow addition alongside [`@typescript-eslint/require-array-sort-compare`](https://typescript-eslint.io/rules/require-array-sort-compare/) (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix — descending order is a real, if less common, alternative intent. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
391
393
  | `prefer-options-object-param` | suggestion | **A run of 2+ trailing optional parameters (`?`-marked or default-valued) should be bundled into one destructured `options` parameter.** Without this, a caller needing only the last optional parameter must still pass `undefined` for every optional parameter before it (the motivating case: a 10-parameter constructor with 8 trailing optional ones). The threshold is configurable (`{ minTrailingOptional }`, default `2`). A suggestion (not a full autofix) rewrites the parameter list and inserts a `const { ... } = options ?? {};` destructure as the body's first statement; call sites are never rewritten, since the signature edit alone turns every stale positional call site into a real compile error. Still reported, but with no suggestion offered, whenever collapsing the run would not be safe/mechanical: a parameter property in the run, a parameter with no simple resolvable name and explicit type of its own (including a destructured parameter, or one typed only through a separately-declared function-type alias), a decorated parameter, a rest parameter anywhere in the full parameter list, no `BlockStatement` body at all (an arrow's expression body, or any declaration-only signature — including an ambient `.d.ts` function, an interface method/construct signature, or an abstract/ambient class method), a `@param` JSDoc tag naming a parameter in the run, or a parameter/function-scope-local variable already named `options`. Complements `max-params` (see "Individual rule tuning" above), which catches the different shape (excessive REQUIRED parameters) this rule is deliberately blind to. Requires no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export, like `prefer-readonly-array-param` above; unlike its own `prefer-readonly-object-param`/`prefer-numeric-sort-compare` siblings, which genuinely need the checker and so cannot be offered outside the default export at all. |
392
- | `prefer-doc-comment` | ✓ | **A substantial leading comment on an exported declaration should be a doc comment, not a plain `//`/`/* */` comment.** "Substantial" means 2 or more comment lines, or a single line whose text exceeds a configurable length (`{ maxLineLength }`, default `80`). Covers an exported function declaration, an exported function's own signature (a body-less `TSDeclareFunction`, an ordinary overload signature and a genuinely ambient one alike, `declare function foo(): void;` included, since TypeScript never shows either one's own implementation to callers, only the signature itself), an exported `const` declaration, whatever its initialiser (a function expression or arrow function, an ordinary value such as `export const TIMEOUT_MS = 5000;`, or none at all in an ambient `export declare const x: number;`; reported once even when the statement declares several, e.g. `export const h1 = () => {}, h2 = () => {};`), an exported class declaration, ambient (`export declare class`) or ordinary alike (TypeScript strips only an ambient class's own MEMBER bodies, never its declaration shape, so its leading comment documents the identical public contract an ordinary exported class's does), an exported class's public (non-`private`/`protected`/`#`-private) method (covering both a method's own overload signature and its trailing implementation) and public abstract method (`abstract m(): void;`, needing no equivalent overload-implementation exemption of its own, since TypeScript's own grammar never lets an abstract method carry a body at all), an exported class's public function/arrow-valued property (the class-member equivalent of the function-initialised `const` shape above), an exported interface/type-alias declaration, an exported enum declaration (`const enum` alike; never an individual member, the identical reason an interface's members are never individually checked), an exported namespace/module declaration (ambient or ordinary alike, at any nesting depth whose enclosing namespaces are all exported themselves), and a default-exported expression (`export default (): number => 1;`, `export default 42;`; a default-exported `function`/`class` declaration or body-less signature is covered by its own shape above and reported exactly once, never twice); a non-exported declaration is never reported, regardless of its comment. A TypeScript function/method overload set's own body-carrying IMPLEMENTATION signature is never reported either, however substantial its own comment: TypeScript never shows that signature to callers at all, only the separate overload signatures above it (a body-less `TSDeclareFunction` for a function, written without `declare`, or a `MethodDefinition` whose own `value` is `TSEmptyBodyFunctionExpression` for a class method) are ever checked against a call site, and both of those are themselves reported like any other declaration in this rule's scope, as the overload set's own real public contract; a comment directly above the implementation is instead internal reasoning about how the overloads are actually realised, never a public contract. The implementation is detected via ESLint's own scope analysis for a function (the overload signatures and the implementation all share one Variable) and, for a class method, via an EARLIER (source-order), same-`static`-ness, same-key signature sibling: a same-named `static`/instance pair is never one overload set to TypeScript itself, and a same-named signature declared AFTER the implementation belongs to a different, later member, never the one the implementation itself realises, so neither is ever mistaken for the implementation's own overload signature. A trailing comment on the previous line of code (`const x = 1; // note`) is never treated as the next declaration's own leading comment. A directive-shaped line (the full ESLint directive/config-comment family, `eslint-disable`/`eslint-disable-line`/`eslint-disable-next-line`/`eslint-enable`/`eslint`/`eslint-env`/`global`/`globals`/`exported`, matched case-sensitively, exactly as ESLint's own directive parser matches them, each requiring real whitespace or end-of-string immediately after the keyword rather than a bare word boundary, so an ordinary compound word such as `eslint-plugin-x` is never mistaken for one, and, on a `//` Line comment specifically, recognised only as `eslint-disable-line`/`eslint-disable-next-line`, the two labels ESLint itself honours there; every other member of the family is a directive only in Block form, matching what ESLint itself does; `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, the `ts-*` markers matched with or without their own leading `@`; `TODO`/`FIXME`, matched case-insensitively when all-uppercase or immediately followed by `:`/`(` (`todo:`, `fixme(scope):`), never a `Todo`/`Fixme` opening ordinary prose; `prettier-ignore`; `c8 ignore`/`v8 ignore`/`istanbul ignore`/`node:coverage ignore`/`node:coverage disable`/`node:coverage enable`; `cspell:disable`/`cspell:disable-line`/`cspell:disable-next-line`/`cspell:enable` (never cspell's own `ignore`/`words` directives, which name dictionary words rather than suppressing a stretch of text); `biome-ignore`; and `#region`/`#endregion`, an editor folding marker) found anywhere in a `//` run is never itself merged into the fix: only the prose segment directly adjacent to the declaration, the run of lines after the LAST such directive, is ever reported and converted; every earlier segment, and every directive line itself, is left completely untouched, since an earlier segment sits on the far side of a directive from the declaration and documents whatever the directive itself demarcates, never the declaration below it; a directive seated directly above the declaration itself is transparent to this split rather than a boundary of it, since group assembly skips past it before the run is ever collected, so the prose above it is still reported and converted, with the directive preserved untouched in place, still functional, directly above the declaration; a directive found inside an already-consolidated Block comment leaves the whole comment alone, since a partial fix cannot safely be sliced out of one physical comment node. A `///` triple-slash reference/AMD directive (`/// <reference types="..." />`) is recognised separately, by its own un-stripped comment value rather than this same pattern, and excluded from the fix the same way. An existing `/**`-opening doc comment, or a `/*!` license/banner block, is left alone regardless of length. Autofix rewraps the comment's existing text into a `/** ... */` block, one line per original logical line as this rule's own fixer writes it, preserving each line's own indentation beyond the delimiter's single conventional space (never flattening a nested example or sub-list to its neighbours' level), and unwraps a hand-written starred block's (`/* ... */` with its own leading `* ` per line) marker rather than doubling it against the fixer's own. This describes this rule's own fixer alone, not a guarantee that `exadevConfig()`'s combined output preserves that text verbatim: a bundled sibling rule's own fixer can still rewrite it further in the same `--fix` run, which is exactly why the fix is withheld below for the two such collisions this rule already guards against. Handles the leading comment whether it is a run of `//` lines or already a bare block: a bare block may be hand-written, or produced by the bare-block fixer of `exadev/multiline-comment-style` (bundled alongside this rule) or of a consumer's own `@stylistic/multiline-comment-style`. Always reported when substantial, but the fix withheld, rather than silently skipping the violation, whenever the exact resulting text would contain a literal closing-comment delimiter or fails to parse as valid TSDoc (checked directly against `@microsoft/tsdoc`'s own parser, the same one `eslint-plugin-tsdoc`'s `tsdoc/syntax` validates against). Also withheld when any considered line itself begins with a literal `*` once its own leading whitespace is trimmed (a hand-written markdown bullet, `* item`, or a bare block comment whose own lines are only partially starred): spliced verbatim into this fixer's own template, that reads as a repeated delimiter on a middle line, which eslint-plugin-jsdoc's own `jsdoc/no-multi-asterisks` rule (bundled unconditionally alongside this rule, see [`jsdoc.ts`](src/jsdoc.ts)) would then strip in the very same `--fix` run, silently losing real content in a way neither this rule nor `@microsoft/tsdoc`'s own parser has any way to detect on its own. Also withheld when any considered line carries an unescaped tag-shaped `@word` mention, at the line's own start (a genuine TSDoc block or modifier tag, `@remarks`, `@param foo`, `@internal`, `@typeParam T`, `@virtual`, `@override`, ...) or immediately after whitespace anywhere further into the line (a bare mid-line mention, `tseslint.config() is @deprecated upstream`), the identical shape `jsdoc/escape-inline-tags` itself keys on: a line-start tag withholds because the converted doc comment would be validated by every tag-checking sibling rule in the bundle and read by TypeScript's own JSDoc parser, and this rule cannot enumerate in advance which tag either will next object to or act on. jsdoc.ts gives `jsdoc/check-tag-names` the TSDoc vocabulary (`settings.jsdoc.tagNamePreference` entries for the TSDoc tags JSDoc spells differently, `typed: false`, and `jsdoc/empty-tags` off), so the TSDoc tags that once collided (`@internal`, `@public`, `@readonly`, `@typeParam`, `@virtual`, `@override`) survive `--fix` there, but a converted tag line such as `@deprecated` or `@internal` is still read as a REAL tag on the symbol, changing what it means rather than merely its formatting. Withholding on every tag-shaped line, not merely a blank line adjacent to one, is deliberate, so no further one-off guard is needed each time a new collision is found. Requires no type information. Wired only through `stylisticCommentsConfig` into the default (type-checked) `exadevConfig()` output (see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules)), never registered in `plugin.configs.recommended`/`.barrel`, so a consumer using the lighter `extends: [plugin.configs.recommended]` option (see [The lighter option](#the-lighter-option-the-plugin-named-export)) never gets it. |
393
- | `multiline-comment-style` | ✓ | **`@stylistic/eslint-plugin`'s [`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style), never merging a directive into a block comment.** Delegates to the upstream rule with the same options, defaults, messages and fixer, but hides every line `prefer-doc-comment` treats as a directive (`TODO`/`FIXME` task markers, `cspell:*`, `biome-ignore`, `#region`/`#endregion` and the rest of that rule's list) from the upstream rule's grouping, so such a line breaks a `//` run the way upstream's own directives do. Exists because upstream does not yet treat `TODO`/`FIXME` as directives ([eslint-stylistic/eslint-stylistic#1287](https://github.com/eslint-stylistic/eslint-stylistic/issues/1287)); see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules). Requires no type information. Wired at `'bare-block'` only through `stylisticCommentsConfig` into the default `exadevConfig()` output, never in `plugin.configs.recommended`/`.barrel`. |
394
+ | `prefer-doc-comment` | ✓ | **A substantial leading comment on an exported declaration should be a doc comment, not a plain `//`/`/* */` comment.** "Substantial" means 2 or more comment lines, or a single line whose text exceeds a configurable length (`{ maxLineLength }`, default `80`). Covers an exported function declaration, an exported function's own signature (a body-less `TSDeclareFunction`, an ordinary overload signature and a genuinely ambient one alike, `declare function foo(): void;` included, since TypeScript never shows either one's own implementation to callers, only the signature itself), an exported `const` declaration, whatever its initialiser (a function expression or arrow function, an ordinary value such as `export const TIMEOUT_MS = 5000;`, or none at all in an ambient `export declare const x: number;`; reported once even when the statement declares several, e.g. `export const h1 = () => {}, h2 = () => {};`), an exported class declaration, ambient (`export declare class`) or ordinary alike (TypeScript strips only an ambient class's own MEMBER bodies, never its declaration shape, so its leading comment documents the identical public contract an ordinary exported class's does), an exported class's public (non-`private`/`protected`/`#`-private) method (covering both a method's own overload signature and its trailing implementation) and public abstract method (`abstract m(): void;`, needing no equivalent overload-implementation exemption of its own, since TypeScript's own grammar never lets an abstract method carry a body at all), an exported class's public function/arrow-valued property (the class-member equivalent of the function-initialised `const` shape above), an exported interface/type-alias declaration, an exported enum declaration (`const enum` alike; never an individual member, the identical reason an interface's members are never individually checked), an exported namespace/module declaration (ambient or ordinary alike, at any nesting depth whose enclosing namespaces are all exported themselves), and a default-exported expression (`export default (): number => 1;`, `export default 42;`; a default-exported `function`/`class` declaration or body-less signature is covered by its own shape above and reported exactly once, never twice); a non-exported declaration is never reported, regardless of its comment. A TypeScript function/method overload set's own body-carrying IMPLEMENTATION signature is never reported either, however substantial its own comment: TypeScript never shows that signature to callers at all, only the separate overload signatures above it (a body-less `TSDeclareFunction` for a function, written without `declare`, or a `MethodDefinition` whose own `value` is `TSEmptyBodyFunctionExpression` for a class method) are ever checked against a call site, and both of those are themselves reported like any other declaration in this rule's scope, as the overload set's own real public contract; a comment directly above the implementation is instead internal reasoning about how the overloads are actually realised, never a public contract. The implementation is detected via ESLint's own scope analysis for a function (the overload signatures and the implementation all share one Variable) and, for a class method, via an EARLIER (source-order), same-`static`-ness, same-key signature sibling: a same-named `static`/instance pair is never one overload set to TypeScript itself, and a same-named signature declared AFTER the implementation belongs to a different, later member, never the one the implementation itself realises, so neither is ever mistaken for the implementation's own overload signature. A trailing comment on the previous line of code (`const x = 1; // note`) is never treated as the next declaration's own leading comment. A directive-shaped line (the full ESLint directive/config-comment family, `eslint-disable`/`eslint-disable-line`/`eslint-disable-next-line`/`eslint-enable`/`eslint`/`eslint-env`/`global`/`globals`/`exported`, matched case-sensitively, exactly as ESLint's own directive parser matches them, each requiring real whitespace or end-of-string immediately after the keyword rather than a bare word boundary, so an ordinary compound word such as `eslint-plugin-x` is never mistaken for one, and, on a `//` Line comment specifically, recognised only as `eslint-disable-line`/`eslint-disable-next-line`, the two labels ESLint itself honours there; every other member of the family is a directive only in Block form, matching what ESLint itself does; `@ts-expect-error`/`@ts-ignore`/`@ts-nocheck`/`@ts-check`, the `ts-*` markers matched with or without their own leading `@`; `TODO`/`FIXME`, matched case-insensitively when all-uppercase or immediately followed by `:`/`(` (`todo:`, `fixme(scope):`), never a `Todo`/`Fixme` opening ordinary prose; `prettier-ignore`; `c8 ignore`/`v8 ignore`/`istanbul ignore`/`node:coverage ignore`/`node:coverage disable`/`node:coverage enable`; `cspell:disable`/`cspell:disable-line`/`cspell:disable-next-line`/`cspell:enable` (never cspell's own `ignore`/`words` directives, which name dictionary words rather than suppressing a stretch of text); `biome-ignore`; `#region`/`#endregion`, an editor folding marker; and a managed-region marker, a comment of exactly `<word>:<word>:start` or `<word>:<word>:end` such as `content:claude:start`, which a tool locates by its exact line) found anywhere in a `//` run is never itself merged into the fix: only the prose segment directly adjacent to the declaration, the run of lines after the LAST such directive, is ever reported and converted; every earlier segment, and every directive line itself, is left completely untouched, since an earlier segment sits on the far side of a directive from the declaration and documents whatever the directive itself demarcates, never the declaration below it; a directive seated directly above the declaration itself is transparent to this split rather than a boundary of it, since group assembly skips past it before the run is ever collected, so the prose above it is still reported and converted, with the directive preserved untouched in place, still functional, directly above the declaration; a directive found inside an already-consolidated Block comment leaves the whole comment alone, since a partial fix cannot safely be sliced out of one physical comment node. A `///` triple-slash reference/AMD directive (`/// <reference types="..." />`) is recognised separately, by its own un-stripped comment value rather than this same pattern, and excluded from the fix the same way. An existing `/**`-opening doc comment, or a `/*!` license/banner block, is left alone regardless of length. Autofix rewraps the comment's existing text into a `/** ... */` block, one line per original logical line as this rule's own fixer writes it, preserving each line's own indentation beyond the delimiter's single conventional space (never flattening a nested example or sub-list to its neighbours' level), and unwraps a hand-written starred block's (`/* ... */` with its own leading `* ` per line) marker rather than doubling it against the fixer's own. This describes this rule's own fixer alone, not a guarantee that `exadevConfig()`'s combined output preserves that text verbatim: a bundled sibling rule's own fixer can still rewrite it further in the same `--fix` run, which is exactly why the fix is withheld below for the two such collisions this rule already guards against. Handles the leading comment whether it is a run of `//` lines or already a bare block: a bare block may be hand-written, or produced by the bare-block fixer of `exadev/multiline-comment-style` (bundled alongside this rule) or of a consumer's own `@stylistic/multiline-comment-style`. Always reported when substantial, but the fix withheld, rather than silently skipping the violation, whenever the exact resulting text would contain a literal closing-comment delimiter or fails to parse as valid TSDoc (checked directly against `@microsoft/tsdoc`'s own parser, the same one `eslint-plugin-tsdoc`'s `tsdoc/syntax` validates against). Also withheld when any considered line itself begins with a literal `*` once its own leading whitespace is trimmed (a hand-written markdown bullet, `* item`, or a bare block comment whose own lines are only partially starred): spliced verbatim into this fixer's own template, that reads as a repeated delimiter on a middle line, which eslint-plugin-jsdoc's own `jsdoc/no-multi-asterisks` rule (bundled unconditionally alongside this rule, see [`jsdoc.ts`](src/jsdoc.ts)) would then strip in the very same `--fix` run, silently losing real content in a way neither this rule nor `@microsoft/tsdoc`'s own parser has any way to detect on its own. Also withheld when any considered line carries an unescaped tag-shaped `@word` mention, at the line's own start (a genuine TSDoc block or modifier tag, `@remarks`, `@param foo`, `@internal`, `@typeParam T`, `@virtual`, `@override`, ...) or immediately after whitespace anywhere further into the line (a bare mid-line mention, `tseslint.config() is @deprecated upstream`), the identical shape `jsdoc/escape-inline-tags` itself keys on: a line-start tag withholds because the converted doc comment would be validated by every tag-checking sibling rule in the bundle and read by TypeScript's own JSDoc parser, and this rule cannot enumerate in advance which tag either will next object to or act on. jsdoc.ts gives `jsdoc/check-tag-names` the TSDoc vocabulary (`settings.jsdoc.tagNamePreference` entries for the TSDoc tags JSDoc spells differently, `typed: false`, and `jsdoc/empty-tags` off), so the TSDoc tags that once collided (`@internal`, `@public`, `@readonly`, `@typeParam`, `@virtual`, `@override`) survive `--fix` there, but a converted tag line such as `@deprecated` or `@internal` is still read as a REAL tag on the symbol, changing what it means rather than merely its formatting. Withholding on every tag-shaped line, not merely a blank line adjacent to one, is deliberate, so no further one-off guard is needed each time a new collision is found. Requires no type information. Wired only through `stylisticCommentsConfig` into the default (type-checked) `exadevConfig()` output (see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules)), never registered in `plugin.configs.recommended`/`.barrel`, so a consumer using the lighter `extends: [plugin.configs.recommended]` option (see [The lighter option](#the-lighter-option-the-plugin-named-export)) never gets it. |
395
+ | `multiline-comment-style` | ✓ | **`@stylistic/eslint-plugin`'s [`multiline-comment-style`](https://eslint.style/rules/multiline-comment-style), never merging a directive into a block comment.** Delegates to the upstream rule with the same options, defaults, messages and fixer, but hides every line `prefer-doc-comment` treats as a directive (`TODO`/`FIXME` task markers, `cspell:*`, `biome-ignore`, `#region`/`#endregion` and the rest of that rule's list) from the upstream rule's grouping, so such a line breaks a `//` run the way upstream's own directives do. A managed-region marker (a comment of exactly `<word>:<word>:start` or `<word>:<word>:end`, such as `content:claude:start`) is treated as a directive too, so no rewrite merges it with other text. The text the fixer writes also has trailing whitespace removed from every line, since upstream leaves padding on blank lines inside a rewritten block. Exists because upstream does not yet treat `TODO`/`FIXME` as directives ([eslint-stylistic/eslint-stylistic#1287](https://github.com/eslint-stylistic/eslint-stylistic/issues/1287)); see [Stylistic comment, class-member and JSX rules](#stylistic-comment-class-member-and-jsx-rules). Requires no type information. Wired at `'bare-block'` only through `stylisticCommentsConfig` into the default `exadevConfig()` output, never in `plugin.configs.recommended`/`.barrel`. |
394
396
  | `package-json-key-order` | ✓ | **Requires `package.json`'s keys to match `syncpack format`'s order.** See [Optional package.json key ordering](#optional-packagejson-key-ordering) — opt-in via `exadevConfig({ packageJsonKeyOrder: true })`, not part of `recommended`/`barrel`. A JSON-language rule (`@eslint/json`'s `json/json`), not a TSESLint one — needs no type information and doesn't apply to any `.ts`/`.js` file. |
395
397
  | `no-uphill-dependency` | | **Enforces a configured workspace's rank, rank-skip, slice and group-isolation boundaries** on every `package.json`'s declared dependencies, plus selector-based dependency constraints (`dependencyConstraints`), with documented per-edge exceptions (`allow`) and exempt target groups (`exemptTargetGroups`). See [Workspace architecture](#workspace-architecture). Opt-in via `exadevConfig({ workspaceArchitecture })` or the standalone `workspaceArchitectureConfig()`, not part of `recommended`/`barrel`. A JSON-language rule, needs no type information. |
396
398
  | `no-dependency-cycle` | | **Disallows a workspace dependency that can reach back to the package declaring it.** A same-rank or same-slice dependency passes `no-uphill-dependency` while still forming a cycle, which this rule catches instead. See [Workspace architecture](#workspace-architecture). |
package/dist/index.cjs CHANGED
@@ -485,7 +485,7 @@ function buildOptionalPluginConfig(preset, options = {}) {
485
485
  }
486
486
  //#endregion
487
487
  //#region package.json
488
- var version = "2.36.0";
488
+ var version = "2.36.2";
489
489
  //#endregion
490
490
  //#region src/react.ts
491
491
  /**
@@ -3696,6 +3696,7 @@ const ESLINT_LINE_HONOURED_LABELS = /* @__PURE__ */ new Set(["eslint-disable-lin
3696
3696
  const TODO_FIXME_UPPERCASE_PATTERN = /^(?:TODO|FIXME)\b/u;
3697
3697
  const TODO_FIXME_MARKED_PATTERN = /^(?:todo|fixme)\b[:(]/iu;
3698
3698
  const OTHER_DIRECTIVE_PATTERN = /^(?:@?ts-(?:expect-error|ignore|nocheck|check)|prettier-ignore|(?:c8|v8|istanbul)\s+ignore|node:coverage\s+(?:ignore|disable|enable)|cspell:(?:disable(?:-next-line|-line)?|enable)|biome-ignore|#(?:end)?region)\b/iu;
3699
+ const MARKER_PATTERN = /^[\w-]+:[\w-]+:(?:start|end)\s*$/u;
3699
3700
  const UNESCAPED_TAG_MENTION_PATTERN = /(?:^|\s)@\w/u;
3700
3701
  /**
3701
3702
  * The number of leading space characters every non-blank line in `lines` shares, the amount a shared left margin (a hand-written block's own body indentation, aligned however its author chose) can be stripped from every line at once while leaving any EXTRA indentation a particular line carries beyond that margin (a nested example, a sub-list) exactly as it was, relative to its neighbours. A blank line (all whitespace or empty) never lowers the shared amount: it carries no indentation of its own to compare, only whichever real content lines happen to surround it. Zero for an empty `lines`, or one all-blank: there is no real content to measure a margin from at all (`Math.min` of an empty spread would otherwise return `Infinity`, a value extractCommentLines' own caller never actually observes, since a blank line is always reduced to `''` before this value could apply to it, but a real margin of `Infinity` would misrepresent this function's own general contract to any other, future caller).
@@ -3743,7 +3744,7 @@ function isDirectiveComment(text, commentType) {
3743
3744
  const eslintMatch = ESLINT_FAMILY_PATTERN.exec(trimmed);
3744
3745
  if (eslintMatch !== null) return commentType !== _typescript_eslint_utils.AST_TOKEN_TYPES.Line || ESLINT_LINE_HONOURED_LABELS.has(eslintMatch[0]);
3745
3746
  if (TODO_FIXME_UPPERCASE_PATTERN.test(trimmed) || TODO_FIXME_MARKED_PATTERN.test(trimmed)) return true;
3746
- return OTHER_DIRECTIVE_PATTERN.test(trimmed);
3747
+ return MARKER_PATTERN.test(trimmed) || OTHER_DIRECTIVE_PATTERN.test(trimmed);
3747
3748
  }
3748
3749
  /**
3749
3750
  * Whether `comment` is a `///` triple-slash directive (`/// <reference types="..." />`, `/// <reference path="..." />`, `/// <reference lib="..." />`, `/// <amd-module name="..." />`, ...), TypeScript's own ambient-reference/AMD-module syntax, never itself the symbol's documentation, so it must never be folded into a doc comment any more than an `eslint-disable`/`@ts-expect-error`/`prettier-ignore` line is. Checked against `comment`'s own UN-stripped `value` specifically, never the extracted, delimiter-space-stripped line isDirectiveComment's own patterns are matched against: the tokeniser leaves a genuine `///` comment's `value` starting directly with the lone third slash and no space at all (`/ <reference ... />`), while an ordinary `//` comment that merely happens to start its own prose with a slash (`// / test`, `// /etc/passwd`) always carries the single conventional space between the `//` delimiter and that content, so its own `value` starts with a space, never a bare `/`. Testing the raw `value` is what tells the two apart; testing the already-stripped extracted line could not, since stripSingleLeadingSpace would have removed that one distinguishing space from the ordinary-prose case too.
@@ -4003,6 +4004,38 @@ function withoutDirectives(sourceCode) {
4003
4004
  return value.bind(target);
4004
4005
  } });
4005
4006
  }
4007
+ const TRAILING_WHITESPACE_PATTERN = /[ \t]+$/gmu;
4008
+ const TEXT_WRITING_FIXER_METHODS = /* @__PURE__ */ new Set([
4009
+ "insertTextAfter",
4010
+ "insertTextAfterRange",
4011
+ "insertTextBefore",
4012
+ "insertTextBeforeRange",
4013
+ "replaceText",
4014
+ "replaceTextRange"
4015
+ ]);
4016
+ /**
4017
+ * `fixer` with trailing whitespace removed from the text of every edit made through it.
4018
+ *
4019
+ * The upstream fixer builds a rewritten comment line by line and leaves the padding it puts after the comment prefix on a blank line (`" * "`, `"// "`, or the indent under a bare block), plus any padding the source carried before a closing delimiter. Only the text written is touched, never the file, so no line outside the rewritten comment changes.
4020
+ */
4021
+ function withoutTrailingWhitespaceFixer(fixer) {
4022
+ return new Proxy(fixer, { get(target, property) {
4023
+ const value = Reflect.get(target, property, target);
4024
+ if (typeof value !== "function" || !TEXT_WRITING_FIXER_METHODS.has(property)) return value;
4025
+ return (range, text) => {
4026
+ return Reflect.apply(value, target, [range, text.replace(TRAILING_WHITESPACE_PATTERN, "")]);
4027
+ };
4028
+ } });
4029
+ }
4030
+ /**
4031
+ * `fix` run against a fixer that strips trailing whitespace from the text it writes, or `fix` itself when a report carries none (`undefined` or `null`).
4032
+ *
4033
+ * Exported for direct testing: the upstream rule always supplies a fix and may return `null` from it, so neither of those two branches is reachable through a lint run.
4034
+ */
4035
+ function withoutTrailingWhitespace(fix) {
4036
+ if (fix === void 0 || fix === null) return fix;
4037
+ return (fixer) => fix(withoutTrailingWhitespaceFixer(fixer));
4038
+ }
4006
4039
  /**
4007
4040
  * `@stylistic/eslint-plugin`'s `multiline-comment-style`, never merging a comment `exadev/prefer-doc-comment` treats as a directive into a block: the upstream rule's own `create()`, `meta` (options schema, defaults, messages, fixability) and fixer, run against a context whose source code hides those comments from the rule's grouping.
4008
4041
  *
@@ -4012,9 +4045,15 @@ const multilineCommentStyle = {
4012
4045
  ...upstreamRule,
4013
4046
  create(context) {
4014
4047
  const sourceCode = withoutDirectives(context.sourceCode);
4015
- return upstreamRule.create(new Proxy(context, { get(target, property, receiver) {
4048
+ return upstreamRule.create(new Proxy({ ...context }, { get(_target, property, receiver) {
4016
4049
  if (property === "sourceCode") return sourceCode;
4017
- return Reflect.get(target, property, receiver);
4050
+ if (property === "report") return (descriptor) => {
4051
+ context.report({
4052
+ ...descriptor,
4053
+ fix: withoutTrailingWhitespace(descriptor.fix)
4054
+ });
4055
+ };
4056
+ return Reflect.get(context, property, receiver);
4018
4057
  } }));
4019
4058
  }
4020
4059
  };
@@ -11360,6 +11399,7 @@ function importPolicyConfig(policies) {
11360
11399
  //#endregion
11361
11400
  //#region src/jsdoc.ts
11362
11401
  const JS_TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
11402
+ const TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts}";
11363
11403
  const jsdocConfig = eslint_plugin_jsdoc.default.configs["flat/recommended-tsdoc-error"];
11364
11404
  const REQUIREMENTS_TIER_RULES = [
11365
11405
  "jsdoc/require-example",
@@ -11386,21 +11426,29 @@ const TSDOC_TAG_NAME_PREFERENCE = {
11386
11426
  typeParam: "typeParam",
11387
11427
  virtual: "virtual"
11388
11428
  };
11389
- const jsdocAndTsdoc = [{
11390
- files: [JS_TS_FILE_PATTERNS$1],
11391
- ...jsdocConfig,
11392
- settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
11393
- rules: {
11394
- ...jsdocConfig.rules,
11395
- "jsdoc/check-tag-names": ["error", { typed: false }],
11396
- "jsdoc/empty-tags": "off",
11397
- ...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
11429
+ const jsdocAndTsdoc = [
11430
+ {
11431
+ files: [JS_TS_FILE_PATTERNS$1],
11432
+ ...jsdocConfig,
11433
+ settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
11434
+ rules: {
11435
+ ...jsdocConfig.rules,
11436
+ "jsdoc/check-tag-names": ["error", { typed: false }],
11437
+ "jsdoc/empty-tags": "off",
11438
+ "jsdoc/no-types": "off",
11439
+ ...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
11440
+ }
11441
+ },
11442
+ {
11443
+ files: [TS_FILE_PATTERNS$1],
11444
+ rules: { "jsdoc/no-types": "error" }
11445
+ },
11446
+ {
11447
+ files: [TS_FILE_PATTERNS$1],
11448
+ plugins: { tsdoc: eslint_plugin_tsdoc.default },
11449
+ rules: { "tsdoc/syntax": "error" }
11398
11450
  }
11399
- }, {
11400
- files: [JS_TS_FILE_PATTERNS$1],
11401
- plugins: { tsdoc: eslint_plugin_tsdoc.default },
11402
- rules: { "tsdoc/syntax": "error" }
11403
- }];
11451
+ ];
11404
11452
  //#endregion
11405
11453
  //#region src/json-canonical.ts
11406
11454
  /**
@@ -11572,6 +11620,7 @@ function pureModulesConfig(options) {
11572
11620
  //#region src/recommended-type-checked.ts
11573
11621
  const TEST_FILE_PATTERNS = `**/*.{test,spec}.{${TEST_FILE_EXTENSIONS.join(",")}}`;
11574
11622
  const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
11623
+ const TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts}";
11575
11624
  function scopeToJsTs(configs) {
11576
11625
  return configs.map((config) => config.files ? config : {
11577
11626
  ...config,
@@ -11633,7 +11682,6 @@ const recommendedTypeChecked = [
11633
11682
  "@typescript-eslint/require-array-sort-compare": "error",
11634
11683
  "@typescript-eslint/strict-boolean-expressions": "error",
11635
11684
  "@typescript-eslint/strict-void-return": "error",
11636
- "@typescript-eslint/explicit-module-boundary-types": "error",
11637
11685
  "@typescript-eslint/switch-exhaustiveness-check": ["error", {
11638
11686
  considerDefaultExhaustiveForUnions: false,
11639
11687
  requireDefaultForNonUnion: true
@@ -11651,6 +11699,10 @@ const recommendedTypeChecked = [
11651
11699
  }]
11652
11700
  }
11653
11701
  },
11702
+ {
11703
+ files: [TS_FILE_PATTERNS],
11704
+ rules: { "@typescript-eslint/explicit-module-boundary-types": "error" }
11705
+ },
11654
11706
  {
11655
11707
  files: [TEST_FILE_PATTERNS],
11656
11708
  rules: {
package/dist/index.js CHANGED
@@ -450,7 +450,7 @@ function buildOptionalPluginConfig(preset, options = {}) {
450
450
  }
451
451
  //#endregion
452
452
  //#region package.json
453
- var version = "2.36.0";
453
+ var version = "2.36.2";
454
454
  //#endregion
455
455
  //#region src/react.ts
456
456
  /**
@@ -3661,6 +3661,7 @@ const ESLINT_LINE_HONOURED_LABELS = /* @__PURE__ */ new Set(["eslint-disable-lin
3661
3661
  const TODO_FIXME_UPPERCASE_PATTERN = /^(?:TODO|FIXME)\b/u;
3662
3662
  const TODO_FIXME_MARKED_PATTERN = /^(?:todo|fixme)\b[:(]/iu;
3663
3663
  const OTHER_DIRECTIVE_PATTERN = /^(?:@?ts-(?:expect-error|ignore|nocheck|check)|prettier-ignore|(?:c8|v8|istanbul)\s+ignore|node:coverage\s+(?:ignore|disable|enable)|cspell:(?:disable(?:-next-line|-line)?|enable)|biome-ignore|#(?:end)?region)\b/iu;
3664
+ const MARKER_PATTERN = /^[\w-]+:[\w-]+:(?:start|end)\s*$/u;
3664
3665
  const UNESCAPED_TAG_MENTION_PATTERN = /(?:^|\s)@\w/u;
3665
3666
  /**
3666
3667
  * The number of leading space characters every non-blank line in `lines` shares, the amount a shared left margin (a hand-written block's own body indentation, aligned however its author chose) can be stripped from every line at once while leaving any EXTRA indentation a particular line carries beyond that margin (a nested example, a sub-list) exactly as it was, relative to its neighbours. A blank line (all whitespace or empty) never lowers the shared amount: it carries no indentation of its own to compare, only whichever real content lines happen to surround it. Zero for an empty `lines`, or one all-blank: there is no real content to measure a margin from at all (`Math.min` of an empty spread would otherwise return `Infinity`, a value extractCommentLines' own caller never actually observes, since a blank line is always reduced to `''` before this value could apply to it, but a real margin of `Infinity` would misrepresent this function's own general contract to any other, future caller).
@@ -3708,7 +3709,7 @@ function isDirectiveComment(text, commentType) {
3708
3709
  const eslintMatch = ESLINT_FAMILY_PATTERN.exec(trimmed);
3709
3710
  if (eslintMatch !== null) return commentType !== AST_TOKEN_TYPES.Line || ESLINT_LINE_HONOURED_LABELS.has(eslintMatch[0]);
3710
3711
  if (TODO_FIXME_UPPERCASE_PATTERN.test(trimmed) || TODO_FIXME_MARKED_PATTERN.test(trimmed)) return true;
3711
- return OTHER_DIRECTIVE_PATTERN.test(trimmed);
3712
+ return MARKER_PATTERN.test(trimmed) || OTHER_DIRECTIVE_PATTERN.test(trimmed);
3712
3713
  }
3713
3714
  /**
3714
3715
  * Whether `comment` is a `///` triple-slash directive (`/// <reference types="..." />`, `/// <reference path="..." />`, `/// <reference lib="..." />`, `/// <amd-module name="..." />`, ...), TypeScript's own ambient-reference/AMD-module syntax, never itself the symbol's documentation, so it must never be folded into a doc comment any more than an `eslint-disable`/`@ts-expect-error`/`prettier-ignore` line is. Checked against `comment`'s own UN-stripped `value` specifically, never the extracted, delimiter-space-stripped line isDirectiveComment's own patterns are matched against: the tokeniser leaves a genuine `///` comment's `value` starting directly with the lone third slash and no space at all (`/ <reference ... />`), while an ordinary `//` comment that merely happens to start its own prose with a slash (`// / test`, `// /etc/passwd`) always carries the single conventional space between the `//` delimiter and that content, so its own `value` starts with a space, never a bare `/`. Testing the raw `value` is what tells the two apart; testing the already-stripped extracted line could not, since stripSingleLeadingSpace would have removed that one distinguishing space from the ordinary-prose case too.
@@ -3968,6 +3969,38 @@ function withoutDirectives(sourceCode) {
3968
3969
  return value.bind(target);
3969
3970
  } });
3970
3971
  }
3972
+ const TRAILING_WHITESPACE_PATTERN = /[ \t]+$/gmu;
3973
+ const TEXT_WRITING_FIXER_METHODS = /* @__PURE__ */ new Set([
3974
+ "insertTextAfter",
3975
+ "insertTextAfterRange",
3976
+ "insertTextBefore",
3977
+ "insertTextBeforeRange",
3978
+ "replaceText",
3979
+ "replaceTextRange"
3980
+ ]);
3981
+ /**
3982
+ * `fixer` with trailing whitespace removed from the text of every edit made through it.
3983
+ *
3984
+ * The upstream fixer builds a rewritten comment line by line and leaves the padding it puts after the comment prefix on a blank line (`" * "`, `"// "`, or the indent under a bare block), plus any padding the source carried before a closing delimiter. Only the text written is touched, never the file, so no line outside the rewritten comment changes.
3985
+ */
3986
+ function withoutTrailingWhitespaceFixer(fixer) {
3987
+ return new Proxy(fixer, { get(target, property) {
3988
+ const value = Reflect.get(target, property, target);
3989
+ if (typeof value !== "function" || !TEXT_WRITING_FIXER_METHODS.has(property)) return value;
3990
+ return (range, text) => {
3991
+ return Reflect.apply(value, target, [range, text.replace(TRAILING_WHITESPACE_PATTERN, "")]);
3992
+ };
3993
+ } });
3994
+ }
3995
+ /**
3996
+ * `fix` run against a fixer that strips trailing whitespace from the text it writes, or `fix` itself when a report carries none (`undefined` or `null`).
3997
+ *
3998
+ * Exported for direct testing: the upstream rule always supplies a fix and may return `null` from it, so neither of those two branches is reachable through a lint run.
3999
+ */
4000
+ function withoutTrailingWhitespace(fix) {
4001
+ if (fix === void 0 || fix === null) return fix;
4002
+ return (fixer) => fix(withoutTrailingWhitespaceFixer(fixer));
4003
+ }
3971
4004
  /**
3972
4005
  * `@stylistic/eslint-plugin`'s `multiline-comment-style`, never merging a comment `exadev/prefer-doc-comment` treats as a directive into a block: the upstream rule's own `create()`, `meta` (options schema, defaults, messages, fixability) and fixer, run against a context whose source code hides those comments from the rule's grouping.
3973
4006
  *
@@ -3977,9 +4010,15 @@ const multilineCommentStyle = {
3977
4010
  ...upstreamRule,
3978
4011
  create(context) {
3979
4012
  const sourceCode = withoutDirectives(context.sourceCode);
3980
- return upstreamRule.create(new Proxy(context, { get(target, property, receiver) {
4013
+ return upstreamRule.create(new Proxy({ ...context }, { get(_target, property, receiver) {
3981
4014
  if (property === "sourceCode") return sourceCode;
3982
- return Reflect.get(target, property, receiver);
4015
+ if (property === "report") return (descriptor) => {
4016
+ context.report({
4017
+ ...descriptor,
4018
+ fix: withoutTrailingWhitespace(descriptor.fix)
4019
+ });
4020
+ };
4021
+ return Reflect.get(context, property, receiver);
3983
4022
  } }));
3984
4023
  }
3985
4024
  };
@@ -11325,6 +11364,7 @@ function importPolicyConfig(policies) {
11325
11364
  //#endregion
11326
11365
  //#region src/jsdoc.ts
11327
11366
  const JS_TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
11367
+ const TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts}";
11328
11368
  const jsdocConfig = jsdoc.configs["flat/recommended-tsdoc-error"];
11329
11369
  const REQUIREMENTS_TIER_RULES = [
11330
11370
  "jsdoc/require-example",
@@ -11351,21 +11391,29 @@ const TSDOC_TAG_NAME_PREFERENCE = {
11351
11391
  typeParam: "typeParam",
11352
11392
  virtual: "virtual"
11353
11393
  };
11354
- const jsdocAndTsdoc = [{
11355
- files: [JS_TS_FILE_PATTERNS$1],
11356
- ...jsdocConfig,
11357
- settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
11358
- rules: {
11359
- ...jsdocConfig.rules,
11360
- "jsdoc/check-tag-names": ["error", { typed: false }],
11361
- "jsdoc/empty-tags": "off",
11362
- ...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
11394
+ const jsdocAndTsdoc = [
11395
+ {
11396
+ files: [JS_TS_FILE_PATTERNS$1],
11397
+ ...jsdocConfig,
11398
+ settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
11399
+ rules: {
11400
+ ...jsdocConfig.rules,
11401
+ "jsdoc/check-tag-names": ["error", { typed: false }],
11402
+ "jsdoc/empty-tags": "off",
11403
+ "jsdoc/no-types": "off",
11404
+ ...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
11405
+ }
11406
+ },
11407
+ {
11408
+ files: [TS_FILE_PATTERNS$1],
11409
+ rules: { "jsdoc/no-types": "error" }
11410
+ },
11411
+ {
11412
+ files: [TS_FILE_PATTERNS$1],
11413
+ plugins: { tsdoc },
11414
+ rules: { "tsdoc/syntax": "error" }
11363
11415
  }
11364
- }, {
11365
- files: [JS_TS_FILE_PATTERNS$1],
11366
- plugins: { tsdoc },
11367
- rules: { "tsdoc/syntax": "error" }
11368
- }];
11416
+ ];
11369
11417
  //#endregion
11370
11418
  //#region src/json-canonical.ts
11371
11419
  /**
@@ -11537,6 +11585,7 @@ function pureModulesConfig(options) {
11537
11585
  //#region src/recommended-type-checked.ts
11538
11586
  const TEST_FILE_PATTERNS = `**/*.{test,spec}.{${TEST_FILE_EXTENSIONS.join(",")}}`;
11539
11587
  const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
11588
+ const TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts}";
11540
11589
  function scopeToJsTs(configs) {
11541
11590
  return configs.map((config) => config.files ? config : {
11542
11591
  ...config,
@@ -11598,7 +11647,6 @@ const recommendedTypeChecked = [
11598
11647
  "@typescript-eslint/require-array-sort-compare": "error",
11599
11648
  "@typescript-eslint/strict-boolean-expressions": "error",
11600
11649
  "@typescript-eslint/strict-void-return": "error",
11601
- "@typescript-eslint/explicit-module-boundary-types": "error",
11602
11650
  "@typescript-eslint/switch-exhaustiveness-check": ["error", {
11603
11651
  considerDefaultExhaustiveForUnions: false,
11604
11652
  requireDefaultForNonUnion: true
@@ -11616,6 +11664,10 @@ const recommendedTypeChecked = [
11616
11664
  }]
11617
11665
  }
11618
11666
  },
11667
+ {
11668
+ files: [TS_FILE_PATTERNS],
11669
+ rules: { "@typescript-eslint/explicit-module-boundary-types": "error" }
11670
+ },
11619
11671
  {
11620
11672
  files: [TEST_FILE_PATTERNS],
11621
11673
  rules: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exadev-eslint-config",
3
3
  "description": "Shared custom ESLint rules and plugin for ExaDev projects",
4
- "version": "2.36.0",
4
+ "version": "2.36.2",
5
5
  "dependencies": {
6
6
  "@eslint/js": "^10.0.1",
7
7
  "@microsoft/tsdoc": "0.17.0",
@@ -31,7 +31,7 @@
31
31
  "@stryker-mutator/core": "10.0.0",
32
32
  "@stryker-mutator/typescript-checker": "10.0.0",
33
33
  "@stryker-mutator/vitest-runner": "10.0.0",
34
- "@types/node": "^24.9.2",
34
+ "@types/node": "^26.6.4",
35
35
  "@typescript-eslint/rule-tester": "^8.67.0",
36
36
  "@vitest/coverage-v8": "^4.1.10",
37
37
  "@vitest/eslint-plugin": "^1.6.27",