exadev-eslint-config 2.32.0 → 2.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -3
- package/dist/index.cjs +57 -8
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +57 -8
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -233,6 +233,19 @@ React/hooks/a11y and Next.js rule blocks fold in automatically, with no separate
|
|
|
233
233
|
pnpm add -D @next/eslint-plugin-next # Next.js support
|
|
234
234
|
```
|
|
235
235
|
If none of these resolve, `@exadev/eslint-config`'s default export is byte-for-byte identical to the plain TypeScript ruleset — nothing about the base package changes.
|
|
236
|
+
|
|
237
|
+
If you run [`eslint-plugin-depend`](https://github.com/es-tooling/eslint-plugin-depend)'s `ban-dependencies` on your `package.json`, it reports `eslint-plugin-react`, which the [module-replacements](https://github.com/es-tooling/module-replacements) `preferred` preset lists with a [suggested alternative](https://github.com/es-tooling/module-replacements/blob/main/docs/modules/eslint-plugin-react.md); the other React and Next.js peers are not listed. This package's React support is built on `eslint-plugin-react`, and it does not bundle `eslint-plugin-depend`, so allow the package in your own block. `allowed` takes exact package names, and the block needs no `language` of its own because `exadevConfig()` already lints `package.json` as `json/json`:
|
|
238
|
+
```ts
|
|
239
|
+
import { defineConfig } from 'eslint/config';
|
|
240
|
+
import depend from 'eslint-plugin-depend';
|
|
241
|
+
import { exadevConfig } from '@exadev/eslint-config';
|
|
242
|
+
|
|
243
|
+
export default defineConfig(...exadevConfig(), {
|
|
244
|
+
files: ['package.json'],
|
|
245
|
+
plugins: { depend },
|
|
246
|
+
rules: { 'depend/ban-dependencies': ['error', { allowed: ['eslint-plugin-react'] }] },
|
|
247
|
+
});
|
|
248
|
+
```
|
|
236
249
|
2. **For React specifically, the file must actually be `.jsx`/`.tsx`.** The React/hooks/a11y rule block is scoped to `files: ['**/*.jsx', '**/*.tsx']`, so even if `eslint-plugin-react` is resolvable only incidentally (e.g. hoisted as a transitive dependency of something unrelated in a monorepo, with zero real JSX anywhere in the linted project), its rules never match a file that isn't JSX. `@next/eslint-plugin-next`'s own block carries no such glob: its own presence is already an unambiguous signal (nothing installs it except a real Next.js project). The [server component boundary](#server-component-boundary) rules added beside it are scoped to the same JSX files.
|
|
237
250
|
|
|
238
251
|
React support pairs `eslint-plugin-react`'s `flat/recommended` with its own `flat/jsx-runtime` config, turning [`react/react-in-jsx-scope`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md) and [`react/jsx-uses-react`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md) back off. `flat/recommended` alone assumes the classic runtime, where every file using JSX needs `import React` in scope; the automatic JSX runtime (the default since React 17, and the only mode Next.js's own compiler supports) needs no such import. Without this pairing, a consumer on the automatic runtime would see `react/react-in-jsx-scope` fire on every JSX file.
|
|
@@ -372,7 +385,7 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
372
385
|
| `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`. |
|
|
373
386
|
| `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. |
|
|
374
387
|
| `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. |
|
|
375
|
-
| `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, for a consumer who separately enables `@stylistic/eslint-plugin`'s own `multiline-comment-style` themselves, produced by its own bare-block fixer. 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 jsdoc.ts spreads `flat/recommended-tsdoc-error` with no `settings.jsdoc` override of its own, so eslint-plugin-jsdoc's own tag-vocabulary rules (bundled unconditionally alongside this rule too) validate a doc comment's tags against JSDoc/TypeScript's own vocabulary rather than TSDoc's, and confirmed directly (a real combined-config run, not this rule in isolation) to delete, rewrite or otherwise fight over a genuine TSDoc-only tag in the very same `--fix` run: `jsdoc/empty-tags` deletes an `@internal` tag's own description text, `jsdoc/check-tag-names` deletes `@public`/`@readonly` outright and rewrites `@typeParam` to `@template` (which then fails `tsdoc/syntax`, since `@template` is not itself valid TSDoc) and `@virtual` to `@abstract`, and `@override` triggers a live fight between `check-tag-names` and `empty-tags`. A bare mid-line mention withholds because it is worse than a sibling-fixer collision: it parses as perfectly valid TSDoc, but confirmed directly (linting a scratch file with this repo's own bundled config) that TypeScript's own JSDoc parser reads the converted mention as a REAL tag on the symbol, so a converted `is @deprecated upstream` makes every use of the symbol fail `@typescript-eslint/no-deprecated`, changing what the symbol means rather than merely its formatting. A mention wrapped in a backtick code span (`@deprecated` inside one) is exempt from both and converts safely, the backtick immediately before the `@` breaking the whitespace-preceded shape for TypeScript's tag scanning and `escape-inline-tags` alike (both confirmed directly). Withholding on every tag-shaped line, not merely a blank line adjacent to one, is deliberate: this rule has no way to enumerate in advance which further TSDoc-only tag eslint-plugin-jsdoc's own vocabulary will next disagree with, so it withholds structurally rather than adding a further one-off guard each time a new collision is found. `jsdoc.ts`'s own `check-tag-names`/TSDoc-vocabulary disagreement is itself a pre-existing gap on `main`, not caused by this rule, tracked separately. 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. |
|
|
388
|
+
| `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, for a consumer who separately enables `@stylistic/eslint-plugin`'s own `multiline-comment-style` themselves, produced by its own bare-block fixer. 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. |
|
|
376
389
|
| `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. |
|
|
377
390
|
| `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. |
|
|
378
391
|
| `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). |
|
|
@@ -515,11 +528,32 @@ A policy selects files with `files` and `ignores` and holds them to:
|
|
|
515
528
|
- `deny`: `specifiers` the files may not import. `message` is required and should name the requirement (why, and what to do instead), not the rule. `importNames` limits the ban to those imported names (`default` for a default import); a namespace import, a dynamic import, `require` and `export *` take every name, so they are still reported. `allowTypeImports` leaves erased imports alone.
|
|
516
529
|
- `confine`: `specifiers` the files may import only in the files `onlyIn` selects, so the inverse case needs no hand-built complement. `allowTypeImports` and an optional `message` work as for `deny`; the default message lists `onlyIn`.
|
|
517
530
|
- `exceptEdges`: one exact `file` and `specifier` pair with a required `reason`. Neither may contain a glob character, so an exception cannot widen. The specifier is compared exactly as written in that file, and `file` is normalised, so `./src/a.ts` and `src/a.ts` name the same file. Creating the config throws for an exception that could never apply (a file the policy does not select, or a specifier that no `deny` entry selects and no `confine` entry forbids in that file, such as one inside the confine's `onlyIn`), so a stale exception fails instead of lingering.
|
|
531
|
+
- `computedSpecifiers`: what to do with a dynamic `import()` or `require()` whose specifier is built at runtime (`` import(`./${name}`) ``), which no restriction can match. `'ignore'` (the default) skips it; `'report'` reports it in every file the policy selects, so those files can only pull a module in by a specifier the policy can check.
|
|
518
532
|
|
|
519
|
-
Specifiers follow [Specifier patterns](#specifier-patterns). The rule is syntactic and covers `import`, `import type`, `export ... from`, `export * from`, `import x = require()`, dynamic `import()` with a
|
|
533
|
+
Specifiers follow [Specifier patterns](#specifier-patterns). The rule is syntactic and covers `import`, `import type`, `export ... from`, `export * from`, `import x = require()`, dynamic `import()` with a string literal or a template with no substitutions, `require()` with such a string that is not shadowed, and `import('x')` type queries. A specifier built at runtime cannot be judged, so it is skipped unless the policy sets `computedSpecifiers: 'report'`.
|
|
520
534
|
|
|
521
535
|
ESLint's core `no-restricted-imports` covers only the static forms (`import`, `export ... from`, `export * from` and `import x = require()`); it has no check for dynamic `import()`, `require()` or `import('x')` type queries, and `no-restricted-modules` is deprecated since ESLint 7. A preset compiled to `no-restricted-imports` blocks would also inherit flat config's rule-level override: when two policies select the same file, the later block's option list replaces the earlier one's, and an exception edge would have to restate every other restriction of the file. So the preset compiles to one block for a purpose-built rule instead, where every policy that selects a file applies to it. Files outside a policy's `files` are untouched.
|
|
522
536
|
|
|
537
|
+
### Bans that dynamic `import()` bypasses
|
|
538
|
+
|
|
539
|
+
A `no-restricted-imports` ban (core or `@typescript-eslint/no-restricted-imports`, which extends it) reports `import { createFake } from 'pkg/fake'` but not `await import('pkg/fake')` or `` await import(`pkg/fake`) ``, because the rule has no handler for dynamic imports. A lazy route or component is exactly where a test double or a heavy module tends to be pulled in, so a ban meant to keep it out of shipped code should be an import policy, which judges the static, dynamic and `require()` forms with the same options:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
export default defineConfig(
|
|
543
|
+
...exadevConfig(),
|
|
544
|
+
...importPolicyConfig([
|
|
545
|
+
{
|
|
546
|
+
files: ['src/**'],
|
|
547
|
+
ignores: ['src/**/*.test.ts'],
|
|
548
|
+
deny: [{ specifiers: ['pkg/fake', 'src/testing'], message: 'test doubles stay out of shipped code; import them from tests only' }],
|
|
549
|
+
computedSpecifiers: 'report',
|
|
550
|
+
},
|
|
551
|
+
]),
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
The `no-restricted-imports` options map across as follows. A `paths` entry's `name` is a `specifiers` entry, except that a specifier also selects everything beneath it, so `pkg` covers `pkg/sub` as well. A `patterns` entry's `group` is a list of globs in the [specifier pattern](#specifier-patterns) dialect, with a leading `!` excluding. There is no `regex`: the glob dialect's `**`, `*`, `?`, `[...]`, braces and `!` excludes express the sets a ban needs, and a character class (`[Ff]ake`) stands in for case-insensitive matching. `importNames` and `message` carry over, `allowTypeImports` is the same flag, and a namespace import, dynamic import, `require()` or `export *` of a module whose `importNames` are banned is reported, since each takes every name. Avoid the `no-restricted-syntax` workaround (`ImportExpression > Literal[value=/.../]`): flat config replaces a rule's options when a later block sets the same rule for the same files, so that selector silently switches off any other `no-restricted-syntax` list the files already have.
|
|
556
|
+
|
|
523
557
|
## Pure modules
|
|
524
558
|
|
|
525
559
|
A functional core should not touch I/O, read the clock, roll dice or wait on anything. `pureModulesConfig` (or `exadevConfig({ pureModules })`) wires `exadev/pure-module` onto the files that must stay pure:
|
|
@@ -1512,7 +1546,7 @@ pnpm build
|
|
|
1512
1546
|
export type { PlaywrightWiringOptions, PublishWiringOptions, RootTool, RootWiringOptions, StrykerWiringOptions, ToolConfigsWiringOptions, ToolingWiringOptions, VitestWiringOptions } from './tooling-wiring';
|
|
1513
1547
|
export type { EffectiveSeverity, EslintSample, EslintViolation, EslintViolationKind, RequiredSeverity, VerifyEslintOptions } from './verify-eslint';
|
|
1514
1548
|
export type { FileRequirement, PackageCondition, PackageRequirement, PackageRequirementsOptions } from './rules/package-requirements-options';
|
|
1515
|
-
export type { ImportConfine, ImportDeny, ImportExceptEdge, ImportPolicy } from './rules/import-policy-options';
|
|
1549
|
+
export type { ComputedSpecifierHandling, ImportConfine, ImportDeny, ImportExceptEdge, ImportPolicy } from './rules/import-policy-options';
|
|
1516
1550
|
export type { FilenamePatternEntry } from './rules/filename-pattern';
|
|
1517
1551
|
export type { RequiredHeading } from './rules/markdown-required-heading';
|
|
1518
1552
|
export type { RequiredExportsEntry } from './rules/required-exports';
|
package/dist/index.cjs
CHANGED
|
@@ -185,7 +185,7 @@ function buildGitignoreConfig(options = {}) {
|
|
|
185
185
|
}
|
|
186
186
|
//#endregion
|
|
187
187
|
//#region package.json
|
|
188
|
-
var version = "2.
|
|
188
|
+
var version = "2.33.0";
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/is-record.ts
|
|
191
191
|
/**
|
|
@@ -2537,6 +2537,7 @@ function stripNodePrefixFromPattern(pattern) {
|
|
|
2537
2537
|
//#endregion
|
|
2538
2538
|
//#region src/rules/import-policy-options.ts
|
|
2539
2539
|
const OPTION_NAME$20 = "importPolicies";
|
|
2540
|
+
const COMPUTED_SPECIFIER_HANDLINGS = ["ignore", "report"];
|
|
2540
2541
|
const GLOB_CHARACTERS$2 = /[*?[\]{}]/u;
|
|
2541
2542
|
const stringList$1 = {
|
|
2542
2543
|
type: "array",
|
|
@@ -2616,6 +2617,10 @@ const importPoliciesSchema = {
|
|
|
2616
2617
|
],
|
|
2617
2618
|
additionalProperties: false
|
|
2618
2619
|
}
|
|
2620
|
+
},
|
|
2621
|
+
computedSpecifiers: {
|
|
2622
|
+
type: "string",
|
|
2623
|
+
enum: [...COMPUTED_SPECIFIER_HANDLINGS]
|
|
2619
2624
|
}
|
|
2620
2625
|
},
|
|
2621
2626
|
required: ["files"],
|
|
@@ -2632,6 +2637,14 @@ function readBoolean$3(entry, field, name) {
|
|
|
2632
2637
|
if (value !== void 0 && typeof value !== "boolean") throw new Error(`@exadev/eslint-config: "${name}" needs "${field}" to be a boolean.`);
|
|
2633
2638
|
return value;
|
|
2634
2639
|
}
|
|
2640
|
+
function isComputedSpecifierHandling(value) {
|
|
2641
|
+
return COMPUTED_SPECIFIER_HANDLINGS.some((handling) => handling === value);
|
|
2642
|
+
}
|
|
2643
|
+
function readComputedSpecifiers(entry, name) {
|
|
2644
|
+
const value = entry["computedSpecifiers"];
|
|
2645
|
+
if (value === void 0 || isComputedSpecifierHandling(value)) return value;
|
|
2646
|
+
throw new Error(`@exadev/eslint-config: "${name}" needs "computedSpecifiers" to be one of ${COMPUTED_SPECIFIER_HANDLINGS.map((handling) => `"${handling}"`).join(", ")}.`);
|
|
2647
|
+
}
|
|
2635
2648
|
function readDeny(entry, name) {
|
|
2636
2649
|
assertOnlyKeys(entry, [
|
|
2637
2650
|
"specifiers",
|
|
@@ -2686,7 +2699,7 @@ function assertEdgeIsLive(edge, policy) {
|
|
|
2686
2699
|
if (!((policy.deny ?? []).some((deny) => createSpecifierMatcher(deny.specifiers)(edge.specifier, file, root)) || (policy.confine ?? []).some((confine) => createSpecifierMatcher(confine.specifiers)(edge.specifier, file, root) && !createFileScope(confine.onlyIn)(file, root)))) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" exception "${edge.specifier}" in "${edge.file}" is not forbidden there by any deny entry or by any confine entry whose onlyIn excludes the file, so it can never apply.`);
|
|
2687
2700
|
}
|
|
2688
2701
|
/**
|
|
2689
|
-
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2702
|
+
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: `computedSpecifiers`, when given, to be `ignore` or `report`; at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2690
2703
|
*/
|
|
2691
2704
|
function readImportPolicies(value) {
|
|
2692
2705
|
return readEntryRecords(value, OPTION_NAME$20).map((entry) => {
|
|
@@ -2695,13 +2708,15 @@ function readImportPolicies(value) {
|
|
|
2695
2708
|
"ignores",
|
|
2696
2709
|
"deny",
|
|
2697
2710
|
"confine",
|
|
2698
|
-
"exceptEdges"
|
|
2711
|
+
"exceptEdges",
|
|
2712
|
+
"computedSpecifiers"
|
|
2699
2713
|
], OPTION_NAME$20);
|
|
2700
2714
|
const files = readFileGlobs(entry["files"], `${OPTION_NAME$20}.files`);
|
|
2701
2715
|
const ignores = entry["ignores"] === void 0 ? void 0 : readFileGlobs(entry["ignores"], `${OPTION_NAME$20}.ignores`);
|
|
2702
2716
|
const deny = readOptionalList(entry, "deny", OPTION_NAME$20, readDeny);
|
|
2703
2717
|
const confine = readOptionalList(entry, "confine", OPTION_NAME$20, readConfine);
|
|
2704
2718
|
const exceptEdges = readOptionalList(entry, "exceptEdges", OPTION_NAME$20, readExceptEdge);
|
|
2719
|
+
const computedSpecifiers = readComputedSpecifiers(entry, OPTION_NAME$20);
|
|
2705
2720
|
if ((deny === void 0 || deny.length === 0) && (confine === void 0 || confine.length === 0)) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" needs every policy to have at least one "deny" or "confine" entry.`);
|
|
2706
2721
|
const policy = {
|
|
2707
2722
|
files,
|
|
@@ -2712,7 +2727,8 @@ function readImportPolicies(value) {
|
|
|
2712
2727
|
for (const edge of exceptEdges ?? []) assertEdgeIsLive(edge, policy);
|
|
2713
2728
|
return {
|
|
2714
2729
|
...policy,
|
|
2715
|
-
...exceptEdges !== void 0 && { exceptEdges }
|
|
2730
|
+
...exceptEdges !== void 0 && { exceptEdges },
|
|
2731
|
+
...computedSpecifiers !== void 0 && { computedSpecifiers }
|
|
2716
2732
|
};
|
|
2717
2733
|
});
|
|
2718
2734
|
}
|
|
@@ -2768,10 +2784,22 @@ function moduleReferenceOf(node, isRequireGlobal) {
|
|
|
2768
2784
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSImportEqualsDeclaration) return node.moduleReference.type === _typescript_eslint_utils.AST_NODE_TYPES.TSExternalModuleReference ? wholeModule(node, staticString(node.moduleReference.expression), node.importKind === "type") : void 0;
|
|
2769
2785
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.ImportExpression) return wholeModule(node, staticString(node.source), false);
|
|
2770
2786
|
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSImportType) return wholeModule(node, node.source.value, true);
|
|
2787
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2788
|
+
return argument === void 0 ? void 0 : wholeModule(node, staticString(argument), false);
|
|
2789
|
+
}
|
|
2790
|
+
function requireArgument(node, isRequireGlobal) {
|
|
2771
2791
|
if (node.type !== _typescript_eslint_utils.AST_NODE_TYPES.CallExpression) return void 0;
|
|
2772
2792
|
const [argument] = node.arguments;
|
|
2773
2793
|
if (node.callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || node.callee.name !== "require" || node.arguments.length !== 1 || argument === void 0 || !isRequireGlobal(node)) return void 0;
|
|
2774
|
-
return
|
|
2794
|
+
return argument;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Whether the node pulls a module in by a specifier that is not a static string, so no specifier pattern can judge it: a dynamic `import()` or a global `require()` whose argument is anything but a string literal or a substitution-free template. The static-specifier forms are `moduleReferenceOf`'s; a node is never both.
|
|
2798
|
+
*/
|
|
2799
|
+
function hasComputedSpecifier(node, isRequireGlobal) {
|
|
2800
|
+
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.ImportExpression) return staticString(node.source) === void 0;
|
|
2801
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2802
|
+
return argument !== void 0 && staticString(argument) === void 0;
|
|
2775
2803
|
}
|
|
2776
2804
|
function selectsNames(importNames, referenceNames) {
|
|
2777
2805
|
if (importNames === void 0) return true;
|
|
@@ -2796,7 +2824,8 @@ function compilePolicy(policy) {
|
|
|
2796
2824
|
return {
|
|
2797
2825
|
inScope: createFileScope([...policy.files, ...(policy.ignores ?? []).map((glob) => `!${glob}`)]),
|
|
2798
2826
|
restrictions: [...deny, ...confine],
|
|
2799
|
-
exceptEdges: policy.exceptEdges ?? []
|
|
2827
|
+
exceptEdges: policy.exceptEdges ?? [],
|
|
2828
|
+
reportsComputed: policy.computedSpecifiers === "report"
|
|
2800
2829
|
};
|
|
2801
2830
|
}
|
|
2802
2831
|
function isShadowed(sourceCode, call) {
|
|
@@ -2812,7 +2841,10 @@ const importPolicy = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) =>
|
|
|
2812
2841
|
type: "problem",
|
|
2813
2842
|
docs: { description: "Restrict which modules the files matching a glob may import: deny lists, confinement to named files, and exact exception edges." },
|
|
2814
2843
|
schema: [importPoliciesSchema],
|
|
2815
|
-
messages: {
|
|
2844
|
+
messages: {
|
|
2845
|
+
restricted: "\"{{ specifier }}\" cannot be imported here: {{ message }}",
|
|
2846
|
+
computed: "This module specifier is computed at runtime, so the import policy for this file cannot check it. Write it as a string literal."
|
|
2847
|
+
}
|
|
2816
2848
|
},
|
|
2817
2849
|
defaultOptions: [[]],
|
|
2818
2850
|
create(context, [options]) {
|
|
@@ -2820,8 +2852,17 @@ const importPolicy = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) =>
|
|
|
2820
2852
|
const policies = readImportPolicies(options).map(compilePolicy).filter((policy) => policy.inScope(filename, cwd));
|
|
2821
2853
|
if (policies.length === 0) return {};
|
|
2822
2854
|
const relativePath = relativeToCwd(filename, cwd);
|
|
2855
|
+
const reportsComputed = policies.some((policy) => policy.reportsComputed);
|
|
2856
|
+
const isRequireGlobal = (call) => !isShadowed(sourceCode, call);
|
|
2823
2857
|
const check = (node) => {
|
|
2824
|
-
|
|
2858
|
+
if (reportsComputed && hasComputedSpecifier(node, isRequireGlobal)) {
|
|
2859
|
+
context.report({
|
|
2860
|
+
node,
|
|
2861
|
+
messageId: "computed"
|
|
2862
|
+
});
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const reference = moduleReferenceOf(node, isRequireGlobal);
|
|
2825
2866
|
if (reference === void 0) return;
|
|
2826
2867
|
for (const policy of policies) {
|
|
2827
2868
|
if (policy.exceptEdges.some((edge) => edge.file === relativePath && edge.specifier === reference.specifier)) continue;
|
|
@@ -10436,11 +10477,19 @@ const REQUIREMENTS_TIER_RULES = [
|
|
|
10436
10477
|
"jsdoc/require-yields",
|
|
10437
10478
|
"jsdoc/require-yields-type"
|
|
10438
10479
|
];
|
|
10480
|
+
const TSDOC_TAG_NAME_PREFERENCE = {
|
|
10481
|
+
defaultValue: "defaultValue",
|
|
10482
|
+
typeParam: "typeParam",
|
|
10483
|
+
virtual: "virtual"
|
|
10484
|
+
};
|
|
10439
10485
|
const jsdocAndTsdoc = [{
|
|
10440
10486
|
files: [JS_TS_FILE_PATTERNS$1],
|
|
10441
10487
|
...jsdocConfig,
|
|
10488
|
+
settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
|
|
10442
10489
|
rules: {
|
|
10443
10490
|
...jsdocConfig.rules,
|
|
10491
|
+
"jsdoc/check-tag-names": ["error", { typed: false }],
|
|
10492
|
+
"jsdoc/empty-tags": "off",
|
|
10444
10493
|
...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
|
|
10445
10494
|
}
|
|
10446
10495
|
}, {
|
package/dist/index.d.cts
CHANGED
|
@@ -265,6 +265,10 @@ interface ImportExceptEdge {
|
|
|
265
265
|
readonly specifier: string;
|
|
266
266
|
readonly reason: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* What a policy does with a dynamic `import()` or a `require()` whose specifier is not a static string (a variable, a concatenation, or a template with a substitution), which no restriction can match: `ignore` it (the default), or `report` it, so the files the policy selects can only pull modules in by a specifier it can check.
|
|
270
|
+
*/
|
|
271
|
+
type ComputedSpecifierHandling = 'ignore' | 'report';
|
|
268
272
|
/**
|
|
269
273
|
* Import restrictions for the files `files` selects and `ignores` does not. At least one of `deny` and `confine` is required.
|
|
270
274
|
*/
|
|
@@ -274,6 +278,7 @@ interface ImportPolicy {
|
|
|
274
278
|
readonly deny?: readonly ImportDeny[];
|
|
275
279
|
readonly confine?: readonly ImportConfine[];
|
|
276
280
|
readonly exceptEdges?: readonly ImportExceptEdge[];
|
|
281
|
+
readonly computedSpecifiers?: ComputedSpecifierHandling;
|
|
277
282
|
}
|
|
278
283
|
//#endregion
|
|
279
284
|
//#region src/rules/turbo-commands.d.ts
|
|
@@ -524,4 +529,4 @@ interface RequiredImportsEntry {
|
|
|
524
529
|
readonly call?: boolean;
|
|
525
530
|
}
|
|
526
531
|
//#endregion
|
|
527
|
-
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
|
532
|
+
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type ComputedSpecifierHandling, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -265,6 +265,10 @@ interface ImportExceptEdge {
|
|
|
265
265
|
readonly specifier: string;
|
|
266
266
|
readonly reason: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* What a policy does with a dynamic `import()` or a `require()` whose specifier is not a static string (a variable, a concatenation, or a template with a substitution), which no restriction can match: `ignore` it (the default), or `report` it, so the files the policy selects can only pull modules in by a specifier it can check.
|
|
270
|
+
*/
|
|
271
|
+
type ComputedSpecifierHandling = 'ignore' | 'report';
|
|
268
272
|
/**
|
|
269
273
|
* Import restrictions for the files `files` selects and `ignores` does not. At least one of `deny` and `confine` is required.
|
|
270
274
|
*/
|
|
@@ -274,6 +278,7 @@ interface ImportPolicy {
|
|
|
274
278
|
readonly deny?: readonly ImportDeny[];
|
|
275
279
|
readonly confine?: readonly ImportConfine[];
|
|
276
280
|
readonly exceptEdges?: readonly ImportExceptEdge[];
|
|
281
|
+
readonly computedSpecifiers?: ComputedSpecifierHandling;
|
|
277
282
|
}
|
|
278
283
|
//#endregion
|
|
279
284
|
//#region src/rules/turbo-commands.d.ts
|
|
@@ -524,4 +529,4 @@ interface RequiredImportsEntry {
|
|
|
524
529
|
readonly call?: boolean;
|
|
525
530
|
}
|
|
526
531
|
//#endregion
|
|
527
|
-
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
|
532
|
+
export { type AggregateTaskOptions, type AllowedBoundariesIgnore, type AllowedEdge, type BoundaryGroup, type ComputedSpecifierHandling, type DependencyConstraint, type EffectiveSeverity, type EslintSample, type EslintViolation, type EslintViolationKind, type ExemptTargetGroup, type FileRequirement, type FilenamePatternEntry, type GroupSpec, type ImportConfine, type ImportDeny, type ImportExceptEdge, type ImportPolicy, type MarkdownFrontmatter, type MarkdownHeadingsOptions, type NamingOptions, type PackageCondition, type PackageRequirement, type PackageRequirementsOptions, type PackageSelector, type PackageSelectorFields, type PlaywrightWiringOptions, type PublishWiringOptions, type PureModulesOptions, type RankRule, type RankSkipOptions, type RequiredExportsEntry, type RequiredFiles, type RequiredHeading, type RequiredImportsEntry, type RequiredScripts, type RequiredSeverity, type RootTool, type RootWiringOptions, type ScriptContent, type ScriptRequirement, type SliceSpec, type StrykerWiringOptions, type TaskGraphRequirement, type TestHygieneOptions, type ToolConfigsWiringOptions, type ToolingWiringOptions, type TurboBoundariesOptions, type TurboDelegate, type TurboHygieneOptions, type TurboOptions, type VerifyEslintOptions, type VitestWiringOptions, type WorkspaceArchitectureOptions, assertEslintConfig, defaultConfig as default, exadevConfig, importPolicyConfig, markdownHeadingsConfig, publicPlugin as plugin, pureModulesConfig, testHygieneConfig, toolingWiringConfig, turboConfig, verifyEslintConfig, workspaceArchitectureConfig };
|
package/dist/index.js
CHANGED
|
@@ -150,7 +150,7 @@ function buildGitignoreConfig(options = {}) {
|
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
152
|
//#region package.json
|
|
153
|
-
var version = "2.
|
|
153
|
+
var version = "2.33.0";
|
|
154
154
|
//#endregion
|
|
155
155
|
//#region src/is-record.ts
|
|
156
156
|
/**
|
|
@@ -2502,6 +2502,7 @@ function stripNodePrefixFromPattern(pattern) {
|
|
|
2502
2502
|
//#endregion
|
|
2503
2503
|
//#region src/rules/import-policy-options.ts
|
|
2504
2504
|
const OPTION_NAME$20 = "importPolicies";
|
|
2505
|
+
const COMPUTED_SPECIFIER_HANDLINGS = ["ignore", "report"];
|
|
2505
2506
|
const GLOB_CHARACTERS$2 = /[*?[\]{}]/u;
|
|
2506
2507
|
const stringList$1 = {
|
|
2507
2508
|
type: "array",
|
|
@@ -2581,6 +2582,10 @@ const importPoliciesSchema = {
|
|
|
2581
2582
|
],
|
|
2582
2583
|
additionalProperties: false
|
|
2583
2584
|
}
|
|
2585
|
+
},
|
|
2586
|
+
computedSpecifiers: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
enum: [...COMPUTED_SPECIFIER_HANDLINGS]
|
|
2584
2589
|
}
|
|
2585
2590
|
},
|
|
2586
2591
|
required: ["files"],
|
|
@@ -2597,6 +2602,14 @@ function readBoolean$3(entry, field, name) {
|
|
|
2597
2602
|
if (value !== void 0 && typeof value !== "boolean") throw new Error(`@exadev/eslint-config: "${name}" needs "${field}" to be a boolean.`);
|
|
2598
2603
|
return value;
|
|
2599
2604
|
}
|
|
2605
|
+
function isComputedSpecifierHandling(value) {
|
|
2606
|
+
return COMPUTED_SPECIFIER_HANDLINGS.some((handling) => handling === value);
|
|
2607
|
+
}
|
|
2608
|
+
function readComputedSpecifiers(entry, name) {
|
|
2609
|
+
const value = entry["computedSpecifiers"];
|
|
2610
|
+
if (value === void 0 || isComputedSpecifierHandling(value)) return value;
|
|
2611
|
+
throw new Error(`@exadev/eslint-config: "${name}" needs "computedSpecifiers" to be one of ${COMPUTED_SPECIFIER_HANDLINGS.map((handling) => `"${handling}"`).join(", ")}.`);
|
|
2612
|
+
}
|
|
2600
2613
|
function readDeny(entry, name) {
|
|
2601
2614
|
assertOnlyKeys(entry, [
|
|
2602
2615
|
"specifiers",
|
|
@@ -2651,7 +2664,7 @@ function assertEdgeIsLive(edge, policy) {
|
|
|
2651
2664
|
if (!((policy.deny ?? []).some((deny) => createSpecifierMatcher(deny.specifiers)(edge.specifier, file, root)) || (policy.confine ?? []).some((confine) => createSpecifierMatcher(confine.specifiers)(edge.specifier, file, root) && !createFileScope(confine.onlyIn)(file, root)))) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" exception "${edge.specifier}" in "${edge.file}" is not forbidden there by any deny entry or by any confine entry whose onlyIn excludes the file, so it can never apply.`);
|
|
2652
2665
|
}
|
|
2653
2666
|
/**
|
|
2654
|
-
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2667
|
+
* Validates and normalises an import-policy option value at runtime, for the rule and for `importPolicyConfig` alike. Beyond the schema it requires: `computedSpecifiers`, when given, to be `ignore` or `report`; at least one include glob in `files`; at least one of `deny` and `confine` per policy; no unknown key; and that every exception edge names a file the policy selects and a specifier one of its restrictions forbids in that file (a confine entry forbids nothing in the files of its `onlyIn`). The edge's `file` is stored in its normalised, forward-slash form. Throws naming the offending field.
|
|
2655
2668
|
*/
|
|
2656
2669
|
function readImportPolicies(value) {
|
|
2657
2670
|
return readEntryRecords(value, OPTION_NAME$20).map((entry) => {
|
|
@@ -2660,13 +2673,15 @@ function readImportPolicies(value) {
|
|
|
2660
2673
|
"ignores",
|
|
2661
2674
|
"deny",
|
|
2662
2675
|
"confine",
|
|
2663
|
-
"exceptEdges"
|
|
2676
|
+
"exceptEdges",
|
|
2677
|
+
"computedSpecifiers"
|
|
2664
2678
|
], OPTION_NAME$20);
|
|
2665
2679
|
const files = readFileGlobs(entry["files"], `${OPTION_NAME$20}.files`);
|
|
2666
2680
|
const ignores = entry["ignores"] === void 0 ? void 0 : readFileGlobs(entry["ignores"], `${OPTION_NAME$20}.ignores`);
|
|
2667
2681
|
const deny = readOptionalList(entry, "deny", OPTION_NAME$20, readDeny);
|
|
2668
2682
|
const confine = readOptionalList(entry, "confine", OPTION_NAME$20, readConfine);
|
|
2669
2683
|
const exceptEdges = readOptionalList(entry, "exceptEdges", OPTION_NAME$20, readExceptEdge);
|
|
2684
|
+
const computedSpecifiers = readComputedSpecifiers(entry, OPTION_NAME$20);
|
|
2670
2685
|
if ((deny === void 0 || deny.length === 0) && (confine === void 0 || confine.length === 0)) throw new Error(`@exadev/eslint-config: "${OPTION_NAME$20}" needs every policy to have at least one "deny" or "confine" entry.`);
|
|
2671
2686
|
const policy = {
|
|
2672
2687
|
files,
|
|
@@ -2677,7 +2692,8 @@ function readImportPolicies(value) {
|
|
|
2677
2692
|
for (const edge of exceptEdges ?? []) assertEdgeIsLive(edge, policy);
|
|
2678
2693
|
return {
|
|
2679
2694
|
...policy,
|
|
2680
|
-
...exceptEdges !== void 0 && { exceptEdges }
|
|
2695
|
+
...exceptEdges !== void 0 && { exceptEdges },
|
|
2696
|
+
...computedSpecifiers !== void 0 && { computedSpecifiers }
|
|
2681
2697
|
};
|
|
2682
2698
|
});
|
|
2683
2699
|
}
|
|
@@ -2733,10 +2749,22 @@ function moduleReferenceOf(node, isRequireGlobal) {
|
|
|
2733
2749
|
if (node.type === AST_NODE_TYPES.TSImportEqualsDeclaration) return node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference ? wholeModule(node, staticString(node.moduleReference.expression), node.importKind === "type") : void 0;
|
|
2734
2750
|
if (node.type === AST_NODE_TYPES.ImportExpression) return wholeModule(node, staticString(node.source), false);
|
|
2735
2751
|
if (node.type === AST_NODE_TYPES.TSImportType) return wholeModule(node, node.source.value, true);
|
|
2752
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2753
|
+
return argument === void 0 ? void 0 : wholeModule(node, staticString(argument), false);
|
|
2754
|
+
}
|
|
2755
|
+
function requireArgument(node, isRequireGlobal) {
|
|
2736
2756
|
if (node.type !== AST_NODE_TYPES.CallExpression) return void 0;
|
|
2737
2757
|
const [argument] = node.arguments;
|
|
2738
2758
|
if (node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== "require" || node.arguments.length !== 1 || argument === void 0 || !isRequireGlobal(node)) return void 0;
|
|
2739
|
-
return
|
|
2759
|
+
return argument;
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Whether the node pulls a module in by a specifier that is not a static string, so no specifier pattern can judge it: a dynamic `import()` or a global `require()` whose argument is anything but a string literal or a substitution-free template. The static-specifier forms are `moduleReferenceOf`'s; a node is never both.
|
|
2763
|
+
*/
|
|
2764
|
+
function hasComputedSpecifier(node, isRequireGlobal) {
|
|
2765
|
+
if (node.type === AST_NODE_TYPES.ImportExpression) return staticString(node.source) === void 0;
|
|
2766
|
+
const argument = requireArgument(node, isRequireGlobal);
|
|
2767
|
+
return argument !== void 0 && staticString(argument) === void 0;
|
|
2740
2768
|
}
|
|
2741
2769
|
function selectsNames(importNames, referenceNames) {
|
|
2742
2770
|
if (importNames === void 0) return true;
|
|
@@ -2761,7 +2789,8 @@ function compilePolicy(policy) {
|
|
|
2761
2789
|
return {
|
|
2762
2790
|
inScope: createFileScope([...policy.files, ...(policy.ignores ?? []).map((glob) => `!${glob}`)]),
|
|
2763
2791
|
restrictions: [...deny, ...confine],
|
|
2764
|
-
exceptEdges: policy.exceptEdges ?? []
|
|
2792
|
+
exceptEdges: policy.exceptEdges ?? [],
|
|
2793
|
+
reportsComputed: policy.computedSpecifiers === "report"
|
|
2765
2794
|
};
|
|
2766
2795
|
}
|
|
2767
2796
|
function isShadowed(sourceCode, call) {
|
|
@@ -2777,7 +2806,10 @@ const importPolicy = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDe
|
|
|
2777
2806
|
type: "problem",
|
|
2778
2807
|
docs: { description: "Restrict which modules the files matching a glob may import: deny lists, confinement to named files, and exact exception edges." },
|
|
2779
2808
|
schema: [importPoliciesSchema],
|
|
2780
|
-
messages: {
|
|
2809
|
+
messages: {
|
|
2810
|
+
restricted: "\"{{ specifier }}\" cannot be imported here: {{ message }}",
|
|
2811
|
+
computed: "This module specifier is computed at runtime, so the import policy for this file cannot check it. Write it as a string literal."
|
|
2812
|
+
}
|
|
2781
2813
|
},
|
|
2782
2814
|
defaultOptions: [[]],
|
|
2783
2815
|
create(context, [options]) {
|
|
@@ -2785,8 +2817,17 @@ const importPolicy = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDe
|
|
|
2785
2817
|
const policies = readImportPolicies(options).map(compilePolicy).filter((policy) => policy.inScope(filename, cwd));
|
|
2786
2818
|
if (policies.length === 0) return {};
|
|
2787
2819
|
const relativePath = relativeToCwd(filename, cwd);
|
|
2820
|
+
const reportsComputed = policies.some((policy) => policy.reportsComputed);
|
|
2821
|
+
const isRequireGlobal = (call) => !isShadowed(sourceCode, call);
|
|
2788
2822
|
const check = (node) => {
|
|
2789
|
-
|
|
2823
|
+
if (reportsComputed && hasComputedSpecifier(node, isRequireGlobal)) {
|
|
2824
|
+
context.report({
|
|
2825
|
+
node,
|
|
2826
|
+
messageId: "computed"
|
|
2827
|
+
});
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
const reference = moduleReferenceOf(node, isRequireGlobal);
|
|
2790
2831
|
if (reference === void 0) return;
|
|
2791
2832
|
for (const policy of policies) {
|
|
2792
2833
|
if (policy.exceptEdges.some((edge) => edge.file === relativePath && edge.specifier === reference.specifier)) continue;
|
|
@@ -10401,11 +10442,19 @@ const REQUIREMENTS_TIER_RULES = [
|
|
|
10401
10442
|
"jsdoc/require-yields",
|
|
10402
10443
|
"jsdoc/require-yields-type"
|
|
10403
10444
|
];
|
|
10445
|
+
const TSDOC_TAG_NAME_PREFERENCE = {
|
|
10446
|
+
defaultValue: "defaultValue",
|
|
10447
|
+
typeParam: "typeParam",
|
|
10448
|
+
virtual: "virtual"
|
|
10449
|
+
};
|
|
10404
10450
|
const jsdocAndTsdoc = [{
|
|
10405
10451
|
files: [JS_TS_FILE_PATTERNS$1],
|
|
10406
10452
|
...jsdocConfig,
|
|
10453
|
+
settings: { jsdoc: { tagNamePreference: TSDOC_TAG_NAME_PREFERENCE } },
|
|
10407
10454
|
rules: {
|
|
10408
10455
|
...jsdocConfig.rules,
|
|
10456
|
+
"jsdoc/check-tag-names": ["error", { typed: false }],
|
|
10457
|
+
"jsdoc/empty-tags": "off",
|
|
10409
10458
|
...Object.fromEntries(REQUIREMENTS_TIER_RULES.map((rule) => [rule, "off"]))
|
|
10410
10459
|
}
|
|
10411
10460
|
}, {
|
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.
|
|
4
|
+
"version": "2.33.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@eslint/js": "^10.0.1",
|
|
7
7
|
"@microsoft/tsdoc": "0.17.0",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
36
36
|
"cosmiconfig": "10.0.1",
|
|
37
37
|
"eslint": "^10.8.0",
|
|
38
|
+
"eslint-plugin-depend": "1.5.0",
|
|
38
39
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
39
40
|
"eslint-plugin-react": "^7.37.5",
|
|
40
41
|
"eslint-plugin-react-hooks": "^7.1.1",
|