@exadev/eslint-config 2.5.0 → 2.6.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 +5 -4
- package/dist/index.cjs +140 -53
- package/dist/index.js +141 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required pee
|
|
|
16
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/no-deprecated`, `@typescript-eslint/no-misused-spread`, `@typescript-eslint/no-mixed-enums`, `@typescript-eslint/no-unnecessary-condition`, `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/use-unknown-in-catch-callback-variable`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), and `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers` is not (see below). Spread it directly into `tseslint.config(...)`:
|
|
19
|
+
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/no-deprecated`, `@typescript-eslint/no-misused-spread`, `@typescript-eslint/no-mixed-enums`, `@typescript-eslint/no-unnecessary-condition`, `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/use-unknown-in-catch-callback-variable`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), and `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers` is not (see below). Spread it directly into `tseslint.config(...)`:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// eslint.config.ts
|
|
@@ -115,9 +115,10 @@ export default tseslint.config(
|
|
|
115
115
|
| `no-object-assign` | ✓/suggestion | `Object.assign` does not check a source object's properties against the target's declared types, unlike object spread. A fresh object-literal target autofixes to `{ ...target, ...source }`; mutating an existing reassignable binding offers a suggestion only (changes the object's identity); a `const` binding or a non-statement call site gets a plain report with no fix. |
|
|
116
116
|
| `no-mutable-union-array-param` | ✓ | A function parameter typed as an array of a union (`(string \| number)[]`) accepts a narrower caller array (`number[]`) by covariance; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` on it can then insert a value the caller's own array was never declared to hold. Autofix marks the parameter `readonly`, turning the mutating call into a real compile error to resolve deliberately. Requires no type information. |
|
|
117
117
|
| `prefer-readonly-array-param` | ✓ | A narrower, safely-autofixable sibling of `@typescript-eslint/prefer-readonly-parameter-types` scoped to array/tuple parameter shapes only: fires unconditionally on every non-readonly array or tuple parameter, regardless of whether the function body mutates it, in any parameter position (a plain identifier, a rest parameter, a default-valued parameter, or a constructor parameter property) and any function-like shape (a concrete function/arrow/method, or a declaration-only ambient function, interface method, function type alias, call/construct signature, or abstract/ambient class method). A union containing an array/tuple member is fixed on that member alone. Autofix prepends `readonly ` (or renames `Array<T>` to `ReadonlyArray<T>`), turning any resulting mutation into a real compile error to resolve deliberately. Requires no type information -- registered in both `plugin.configs.recommended` and the default (type-checked) export. |
|
|
118
|
-
| `
|
|
119
|
-
| `no-
|
|
120
|
-
| `no-
|
|
118
|
+
| `prefer-readonly-object-param` | ✓ | The object-shape sibling of `prefer-readonly-array-param` above, scoped to "flat" object parameters where a shallow fix is provably sufficient: an inline `{ ... }` literal or a reference to a plain named type/interface where every property (and index-signature value, if any) is itself a primitive, a literal/union of primitives, or a callback -- with no nested object, array, tuple, Map, Set, class instance, union, intersection, or unconstrained type parameter anywhere in the shape. Autofix wraps the parameter's own type annotation in `Readonly<...>`, which TypeScript's own deep-readonly check accepts as fully sufficient for a shape this flat. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended` -- to resolve each property's real type via the checker. |
|
|
119
|
+
| `no-array-isarray-mutation` | | `Array.isArray`'s own type declaration narrows to plain `any[]`, discarding the `readonly` guarantee of any array type in the narrowed parameter's or local variable's real type -- a bare `readonly T[]`, a `ReadonlyArray<T>`, one behind a type alias, or one alongside other union members -- inside the guarded branch; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` there can mutate a caller's genuinely readonly array. Recognises the direct `if (Array.isArray(x))` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: re-adding `readonly` is a no-op (the guard already discarded it) and rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended` -- specifically to see through a type alias and to catch a bare, non-union readonly array parameter or local variable, neither visible from its own syntax alone. |
|
|
120
|
+
| `no-map-instanceof-mutation` | | `Map` is declared as extending `ReadonlyMap`, so `instanceof Map` narrows a parameter or local variable whose real type includes a `ReadonlyMap` -- bare, unioned, or reached through a type alias -- straight past the readonly guarantee to the full mutable interface; calling `set`/`delete`/`clear` there can mutate a caller's genuinely read-only map. Recognises the direct `if (input instanceof Map)` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
121
|
+
| `no-set-instanceof-mutation` | | `instanceof Set` narrows a parameter or local variable whose real type includes a `ReadonlySet` -- bare, unioned, or reached through a type alias -- straight to the fully mutable `Set` interface, with no way to preserve the read-only guarantee through the narrowing; calling `add`/`delete`/`clear` there can mutate a caller's genuinely read-only set. Recognises the same guard idioms as `no-map-instanceof-mutation` above. No autofix, for the same aliasing reason. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
121
122
|
| `no-enum-number-widening` | | A bare (non-literal) `number` is accepted anywhere a numeric enum is expected, without checking it is actually one of the enum's members -- only a numeric *literal* gets range-checked by `tsc`. No autofix: the only provably safe fix is a genuine runtime membership check against the enum's own values, which is a behavioural choice a mechanical fix cannot responsibly make. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
122
123
|
| `no-enum-reverse-lookup-widening` | suggestion | 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`. |
|
|
123
124
|
| `prefer-numeric-sort-compare` | suggestion | A deliberately narrow addition alongside `@typescript-eslint/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. |
|
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,7 @@ let typescript = require("typescript");
|
|
|
32
32
|
typescript = __toESM(typescript, 1);
|
|
33
33
|
let ts_api_utils = require("ts-api-utils");
|
|
34
34
|
//#region package.json
|
|
35
|
-
var version = "2.
|
|
35
|
+
var version = "2.6.0";
|
|
36
36
|
//#endregion
|
|
37
37
|
//#region src/rules/barrel-helpers.ts
|
|
38
38
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -276,7 +276,7 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
|
276
276
|
"fill",
|
|
277
277
|
"copyWithin"
|
|
278
278
|
]);
|
|
279
|
-
const createRule$
|
|
279
|
+
const createRule$7 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
280
280
|
function isArrayIsArrayCall(node) {
|
|
281
281
|
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && node.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
282
282
|
}
|
|
@@ -288,33 +288,33 @@ function definitelyExits$2(statement) {
|
|
|
288
288
|
}
|
|
289
289
|
return false;
|
|
290
290
|
}
|
|
291
|
-
const noArrayIsArrayMutation = createRule$
|
|
291
|
+
const noArrayIsArrayMutation = createRule$7({
|
|
292
292
|
name: "no-array-isarray-mutation",
|
|
293
293
|
meta: {
|
|
294
294
|
type: "problem",
|
|
295
295
|
schema: [],
|
|
296
|
-
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
297
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a
|
|
296
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter or local variable whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
297
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a value whose real type includes a readonly array (a caller's array, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
298
298
|
},
|
|
299
299
|
defaultOptions: [],
|
|
300
300
|
create(context) {
|
|
301
301
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
302
302
|
const checker = services.program.getTypeChecker();
|
|
303
|
-
function
|
|
304
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
305
|
-
const
|
|
306
|
-
return (
|
|
303
|
+
function declarationHasReadonlyArrayConstituent(declarationNode) {
|
|
304
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
305
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
306
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
307
307
|
}
|
|
308
308
|
return { CallExpression(node) {
|
|
309
309
|
const { callee } = node;
|
|
310
310
|
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
311
311
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
312
312
|
if (!variable) return;
|
|
313
|
-
const
|
|
314
|
-
if (!
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
if (!
|
|
313
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
314
|
+
if (!declarationDefinition) return;
|
|
315
|
+
const declarationNode = declarationDefinition.name;
|
|
316
|
+
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
317
|
+
if (!declarationHasReadonlyArrayConstituent(declarationNode)) return;
|
|
318
318
|
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
319
319
|
context.report({
|
|
320
320
|
node,
|
|
@@ -496,7 +496,7 @@ const noIndexFiles = {
|
|
|
496
496
|
};
|
|
497
497
|
//#endregion
|
|
498
498
|
//#region src/rules/no-map-instanceof-mutation.ts
|
|
499
|
-
const createRule$
|
|
499
|
+
const createRule$6 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
500
500
|
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
501
501
|
"set",
|
|
502
502
|
"delete",
|
|
@@ -513,33 +513,33 @@ function definitelyExits$1(statement) {
|
|
|
513
513
|
}
|
|
514
514
|
return false;
|
|
515
515
|
}
|
|
516
|
-
const noMapInstanceofMutation = createRule$
|
|
516
|
+
const noMapInstanceofMutation = createRule$6({
|
|
517
517
|
name: "no-map-instanceof-mutation",
|
|
518
518
|
meta: {
|
|
519
519
|
type: "problem",
|
|
520
520
|
schema: [],
|
|
521
|
-
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
522
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a
|
|
521
|
+
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
522
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a value whose real type includes ReadonlyMap (a caller's map, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
|
|
523
523
|
},
|
|
524
524
|
defaultOptions: [],
|
|
525
525
|
create(context) {
|
|
526
526
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
527
527
|
const checker = services.program.getTypeChecker();
|
|
528
|
-
function
|
|
529
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
530
|
-
const
|
|
531
|
-
return (
|
|
528
|
+
function declarationHasReadonlyMapConstituent(declarationNode) {
|
|
529
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
530
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
531
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
|
|
532
532
|
}
|
|
533
533
|
return { CallExpression(node) {
|
|
534
534
|
const { callee } = node;
|
|
535
535
|
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_MAP_METHODS.has(callee.property.name)) return;
|
|
536
536
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
537
537
|
if (!variable) return;
|
|
538
|
-
const
|
|
539
|
-
if (!
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
542
|
-
if (!
|
|
538
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
539
|
+
if (!declarationDefinition) return;
|
|
540
|
+
const declarationNode = declarationDefinition.name;
|
|
541
|
+
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
542
|
+
if (!declarationHasReadonlyMapConstituent(declarationNode)) return;
|
|
543
543
|
if (!isGuardedByInstanceofMap(node, variable, context)) return;
|
|
544
544
|
context.report({
|
|
545
545
|
node,
|
|
@@ -604,7 +604,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
604
604
|
"fill",
|
|
605
605
|
"copyWithin"
|
|
606
606
|
]);
|
|
607
|
-
const createRule$
|
|
607
|
+
const createRule$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
608
608
|
function isUnionArrayType(typeAnnotation) {
|
|
609
609
|
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
610
610
|
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "Array" && typeAnnotation.typeArguments?.params.length === 1) {
|
|
@@ -612,7 +612,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
612
612
|
if (firstParam?.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
const noMutableUnionArrayParam = createRule$
|
|
615
|
+
const noMutableUnionArrayParam = createRule$5({
|
|
616
616
|
name: "no-mutable-union-array-param",
|
|
617
617
|
meta: {
|
|
618
618
|
type: "problem",
|
|
@@ -738,14 +738,14 @@ const noNonBarrelReexport = {
|
|
|
738
738
|
};
|
|
739
739
|
//#endregion
|
|
740
740
|
//#region src/rules/no-object-assign.ts
|
|
741
|
-
const createRule$
|
|
741
|
+
const createRule$4 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
742
742
|
function resolveFrom$1(scope, name) {
|
|
743
743
|
for (let current = scope; current; current = current.upper) {
|
|
744
744
|
const found = current.set.get(name);
|
|
745
745
|
if (found) return found;
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
|
-
const noObjectAssign = createRule$
|
|
748
|
+
const noObjectAssign = createRule$4({
|
|
749
749
|
name: "no-object-assign",
|
|
750
750
|
meta: {
|
|
751
751
|
type: "problem",
|
|
@@ -881,7 +881,7 @@ const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
|
881
881
|
"delete",
|
|
882
882
|
"clear"
|
|
883
883
|
]);
|
|
884
|
-
const createRule$
|
|
884
|
+
const createRule$3 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
885
885
|
function isSetInstanceofExpression(node) {
|
|
886
886
|
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.right.name === "Set";
|
|
887
887
|
}
|
|
@@ -893,33 +893,33 @@ function definitelyExits(statement) {
|
|
|
893
893
|
}
|
|
894
894
|
return false;
|
|
895
895
|
}
|
|
896
|
-
const noSetInstanceofMutation = createRule$
|
|
896
|
+
const noSetInstanceofMutation = createRule$3({
|
|
897
897
|
name: "no-set-instanceof-mutation",
|
|
898
898
|
meta: {
|
|
899
899
|
type: "problem",
|
|
900
900
|
schema: [],
|
|
901
|
-
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
902
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a
|
|
901
|
+
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
902
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a value whose real type includes a ReadonlySet (a caller's set, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
|
|
903
903
|
},
|
|
904
904
|
defaultOptions: [],
|
|
905
905
|
create(context) {
|
|
906
906
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
907
907
|
const checker = services.program.getTypeChecker();
|
|
908
|
-
function
|
|
909
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
910
|
-
const
|
|
911
|
-
return (
|
|
908
|
+
function declarationHasReadonlySetConstituent(declarationNode) {
|
|
909
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
910
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
911
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
|
|
912
912
|
}
|
|
913
913
|
return { CallExpression(node) {
|
|
914
914
|
const { callee } = node;
|
|
915
915
|
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_SET_METHODS.has(callee.property.name)) return;
|
|
916
916
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
917
917
|
if (!variable) return;
|
|
918
|
-
const
|
|
919
|
-
if (!
|
|
920
|
-
const
|
|
921
|
-
if (
|
|
922
|
-
if (!
|
|
918
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
919
|
+
if (!declarationDefinition) return;
|
|
920
|
+
const declarationNode = declarationDefinition.name;
|
|
921
|
+
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
922
|
+
if (!declarationHasReadonlySetConstituent(declarationNode)) return;
|
|
923
923
|
if (!isGuardedBySetInstanceof(node, variable, context)) return;
|
|
924
924
|
context.report({
|
|
925
925
|
node,
|
|
@@ -996,13 +996,13 @@ const noSideEffectsInIndex = {
|
|
|
996
996
|
};
|
|
997
997
|
//#endregion
|
|
998
998
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
999
|
-
const createRule$
|
|
999
|
+
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1000
1000
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
1001
1001
|
function isDefinitelyNumberType(type) {
|
|
1002
1002
|
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
1003
1003
|
return (type.flags & typescript.TypeFlags.NumberLike) !== 0;
|
|
1004
1004
|
}
|
|
1005
|
-
const preferNumericSortCompare = createRule$
|
|
1005
|
+
const preferNumericSortCompare = createRule$2({
|
|
1006
1006
|
name: "prefer-numeric-sort-compare",
|
|
1007
1007
|
meta: {
|
|
1008
1008
|
type: "suggestion",
|
|
@@ -1048,7 +1048,7 @@ const preferNumericSortCompare = createRule$1({
|
|
|
1048
1048
|
});
|
|
1049
1049
|
//#endregion
|
|
1050
1050
|
//#region src/rules/prefer-readonly-array-param.ts
|
|
1051
|
-
const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1051
|
+
const createRule$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1052
1052
|
function getFixableArrayOrTupleType(typeNode) {
|
|
1053
1053
|
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
|
|
1054
1054
|
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return typeNode;
|
|
@@ -1060,12 +1060,12 @@ function getFixableTypesForAnnotation(typeNode) {
|
|
|
1060
1060
|
const fixable = getFixableArrayOrTupleType(typeNode);
|
|
1061
1061
|
return fixable ? [fixable] : [];
|
|
1062
1062
|
}
|
|
1063
|
-
function getAnnotatedParamNode(param) {
|
|
1064
|
-
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(param.parameter);
|
|
1063
|
+
function getAnnotatedParamNode$1(param) {
|
|
1064
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode$1(param.parameter);
|
|
1065
1065
|
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern) return param.left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param.left : void 0;
|
|
1066
1066
|
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement || param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return param;
|
|
1067
1067
|
}
|
|
1068
|
-
const FUNCTION_LIKE_SELECTOR = [
|
|
1068
|
+
const FUNCTION_LIKE_SELECTOR$1 = [
|
|
1069
1069
|
"ArrowFunctionExpression",
|
|
1070
1070
|
"FunctionDeclaration",
|
|
1071
1071
|
"FunctionExpression",
|
|
@@ -1076,7 +1076,7 @@ const FUNCTION_LIKE_SELECTOR = [
|
|
|
1076
1076
|
"TSFunctionType",
|
|
1077
1077
|
"TSMethodSignature"
|
|
1078
1078
|
].join(", ");
|
|
1079
|
-
const preferReadonlyArrayParam = createRule({
|
|
1079
|
+
const preferReadonlyArrayParam = createRule$1({
|
|
1080
1080
|
name: "prefer-readonly-array-param",
|
|
1081
1081
|
meta: {
|
|
1082
1082
|
type: "problem",
|
|
@@ -1088,7 +1088,7 @@ const preferReadonlyArrayParam = createRule({
|
|
|
1088
1088
|
defaultOptions: [],
|
|
1089
1089
|
create(context) {
|
|
1090
1090
|
function checkParam(param) {
|
|
1091
|
-
const annotatedNode = getAnnotatedParamNode(param);
|
|
1091
|
+
const annotatedNode = getAnnotatedParamNode$1(param);
|
|
1092
1092
|
if (!annotatedNode?.typeAnnotation) return;
|
|
1093
1093
|
const fixableTypes = getFixableTypesForAnnotation(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1094
1094
|
if (fixableTypes.length === 0) return;
|
|
@@ -1102,6 +1102,91 @@ const preferReadonlyArrayParam = createRule({
|
|
|
1102
1102
|
}
|
|
1103
1103
|
});
|
|
1104
1104
|
}
|
|
1105
|
+
return { [FUNCTION_LIKE_SELECTOR$1](node) {
|
|
1106
|
+
for (const param of node.params) checkParam(param);
|
|
1107
|
+
} };
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/rules/prefer-readonly-object-param.ts
|
|
1112
|
+
const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1113
|
+
function getAnnotatedParamNode(param) {
|
|
1114
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(param.parameter);
|
|
1115
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern) return param.left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param.left : void 0;
|
|
1116
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement || param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return param;
|
|
1117
|
+
}
|
|
1118
|
+
const FUNCTION_LIKE_SELECTOR = [
|
|
1119
|
+
"ArrowFunctionExpression",
|
|
1120
|
+
"FunctionDeclaration",
|
|
1121
|
+
"FunctionExpression",
|
|
1122
|
+
"TSCallSignatureDeclaration",
|
|
1123
|
+
"TSConstructSignatureDeclaration",
|
|
1124
|
+
"TSDeclareFunction",
|
|
1125
|
+
"TSEmptyBodyFunctionExpression",
|
|
1126
|
+
"TSFunctionType",
|
|
1127
|
+
"TSMethodSignature"
|
|
1128
|
+
].join(", ");
|
|
1129
|
+
function getCandidateTypeNode(typeNode) {
|
|
1130
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeLiteral) return typeNode;
|
|
1131
|
+
if (typeNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference) return void 0;
|
|
1132
|
+
if (typeNode.typeName.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return void 0;
|
|
1133
|
+
if (typeNode.typeName.name === "Readonly") return void 0;
|
|
1134
|
+
return typeNode;
|
|
1135
|
+
}
|
|
1136
|
+
const PRIMITIVE_LIKE_FLAGS = typescript.TypeFlags.StringLike | typescript.TypeFlags.NumberLike | typescript.TypeFlags.BooleanLike | typescript.TypeFlags.BigIntLike | typescript.TypeFlags.ESSymbolLike | typescript.TypeFlags.Null | typescript.TypeFlags.Undefined;
|
|
1137
|
+
function isFlatPropertyType(checker, type) {
|
|
1138
|
+
if (type.isUnion()) return type.types.every((constituent) => isFlatPropertyType(checker, constituent));
|
|
1139
|
+
if ((type.flags & PRIMITIVE_LIKE_FLAGS) !== 0) return true;
|
|
1140
|
+
return checker.getSignaturesOfType(type, typescript.SignatureKind.Call).length > 0 && checker.getPropertiesOfType(type).length === 0 && checker.getIndexInfosOfType(type).length === 0;
|
|
1141
|
+
}
|
|
1142
|
+
function isFlatObjectType(checker, type, location) {
|
|
1143
|
+
if (type.flags & (typescript.TypeFlags.Union | typescript.TypeFlags.Intersection | typescript.TypeFlags.TypeParameter)) return false;
|
|
1144
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
|
1145
|
+
if (checker.getSignaturesOfType(type, typescript.SignatureKind.Call).length > 0) return false;
|
|
1146
|
+
if (checker.getSignaturesOfType(type, typescript.SignatureKind.Construct).length > 0) return false;
|
|
1147
|
+
if ((type.getSymbol()?.flags ?? 0) & typescript.SymbolFlags.Class) return false;
|
|
1148
|
+
const symbolName = type.getSymbol()?.name;
|
|
1149
|
+
if (symbolName === "Map" || symbolName === "ReadonlyMap" || symbolName === "Set" || symbolName === "ReadonlySet") return false;
|
|
1150
|
+
for (const property of checker.getPropertiesOfType(type)) if (!isFlatPropertyType(checker, checker.getTypeOfSymbolAtLocation(property, location))) return false;
|
|
1151
|
+
for (const indexInfo of checker.getIndexInfosOfType(type)) if (!isFlatPropertyType(checker, indexInfo.type)) return false;
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
function isAlreadyFullyReadonly(checker, type) {
|
|
1155
|
+
const properties = checker.getPropertiesOfType(type);
|
|
1156
|
+
const indexInfos = checker.getIndexInfosOfType(type);
|
|
1157
|
+
if (properties.length === 0 && indexInfos.length === 0) return false;
|
|
1158
|
+
return properties.every((property) => (0, ts_api_utils.isPropertyReadonlyInType)(type, property.getEscapedName(), checker)) && indexInfos.every((indexInfo) => indexInfo.isReadonly);
|
|
1159
|
+
}
|
|
1160
|
+
const preferReadonlyObjectParam = createRule({
|
|
1161
|
+
name: "prefer-readonly-object-param",
|
|
1162
|
+
meta: {
|
|
1163
|
+
type: "problem",
|
|
1164
|
+
fixable: "code",
|
|
1165
|
+
docs: { description: "Require a 'flat' object parameter (every property is a primitive or a callback, so a shallow wrapper is provably sufficient) to be typed 'Readonly<T>' -- a narrower, safely-autofixable sibling of @typescript-eslint/prefer-readonly-parameter-types and of this package's own prefer-readonly-array-param, scoped to the object shapes where a shallow fix is genuinely complete." },
|
|
1166
|
+
schema: [],
|
|
1167
|
+
messages: { preferReadonlyObject: "This object parameter is 'flat' -- every property (and index-signature value, if any) is a primitive or a callback, so there is no nested mutable state a shallow wrapper could miss. Wrap it in 'Readonly<...>' so a caller can pass a readonly or shared object with confidence, and so any attempt to mutate it inside the function becomes a deliberate, visible compile error instead of a silent side effect on the caller's data." }
|
|
1168
|
+
},
|
|
1169
|
+
defaultOptions: [],
|
|
1170
|
+
create(context) {
|
|
1171
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
1172
|
+
const checker = services.program.getTypeChecker();
|
|
1173
|
+
function checkParam(param) {
|
|
1174
|
+
const annotatedNode = getAnnotatedParamNode(param);
|
|
1175
|
+
if (!annotatedNode?.typeAnnotation) return;
|
|
1176
|
+
const candidateTypeNode = getCandidateTypeNode(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1177
|
+
if (!candidateTypeNode) return;
|
|
1178
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(annotatedNode);
|
|
1179
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
1180
|
+
if (!isFlatObjectType(checker, type, tsNode)) return;
|
|
1181
|
+
if (isAlreadyFullyReadonly(checker, type)) return;
|
|
1182
|
+
context.report({
|
|
1183
|
+
node: param,
|
|
1184
|
+
messageId: "preferReadonlyObject",
|
|
1185
|
+
fix(fixer) {
|
|
1186
|
+
return [fixer.insertTextBefore(candidateTypeNode, "Readonly<"), fixer.insertTextAfter(candidateTypeNode, ">")];
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1105
1190
|
return { [FUNCTION_LIKE_SELECTOR](node) {
|
|
1106
1191
|
for (const param of node.params) checkParam(param);
|
|
1107
1192
|
} };
|
|
@@ -1131,7 +1216,8 @@ const plugin = {
|
|
|
1131
1216
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1132
1217
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1133
1218
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1134
|
-
"prefer-readonly-array-param": preferReadonlyArrayParam
|
|
1219
|
+
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1220
|
+
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
1135
1221
|
},
|
|
1136
1222
|
configs: {
|
|
1137
1223
|
get recommended() {
|
|
@@ -1176,6 +1262,7 @@ const recommendedTypeChecked = [
|
|
|
1176
1262
|
"exadev/no-set-instanceof-mutation": "error",
|
|
1177
1263
|
"exadev/prefer-numeric-sort-compare": "error",
|
|
1178
1264
|
"exadev/prefer-readonly-array-param": "error",
|
|
1265
|
+
"exadev/prefer-readonly-object-param": "error",
|
|
1179
1266
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
1180
1267
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1181
1268
|
"@typescript-eslint/consistent-type-exports": "error",
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import tseslint from "typescript-eslint";
|
|
|
2
2
|
import { posix } from "node:path";
|
|
3
3
|
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
4
4
|
import * as ts from "typescript";
|
|
5
|
-
import { isTypeReference } from "ts-api-utils";
|
|
5
|
+
import { isPropertyReadonlyInType, isTypeReference } from "ts-api-utils";
|
|
6
6
|
//#region package.json
|
|
7
|
-
var version = "2.
|
|
7
|
+
var version = "2.6.0";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/rules/barrel-helpers.ts
|
|
10
10
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -248,7 +248,7 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
|
248
248
|
"fill",
|
|
249
249
|
"copyWithin"
|
|
250
250
|
]);
|
|
251
|
-
const createRule$
|
|
251
|
+
const createRule$7 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
252
252
|
function isArrayIsArrayCall(node) {
|
|
253
253
|
return node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
254
254
|
}
|
|
@@ -260,33 +260,33 @@ function definitelyExits$2(statement) {
|
|
|
260
260
|
}
|
|
261
261
|
return false;
|
|
262
262
|
}
|
|
263
|
-
const noArrayIsArrayMutation = createRule$
|
|
263
|
+
const noArrayIsArrayMutation = createRule$7({
|
|
264
264
|
name: "no-array-isarray-mutation",
|
|
265
265
|
meta: {
|
|
266
266
|
type: "problem",
|
|
267
267
|
schema: [],
|
|
268
|
-
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
269
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a
|
|
268
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter or local variable whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
269
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a value whose real type includes a readonly array (a caller's array, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
270
270
|
},
|
|
271
271
|
defaultOptions: [],
|
|
272
272
|
create(context) {
|
|
273
273
|
const services = ESLintUtils.getParserServices(context);
|
|
274
274
|
const checker = services.program.getTypeChecker();
|
|
275
|
-
function
|
|
276
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
277
|
-
const
|
|
278
|
-
return (
|
|
275
|
+
function declarationHasReadonlyArrayConstituent(declarationNode) {
|
|
276
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
277
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
278
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
279
279
|
}
|
|
280
280
|
return { CallExpression(node) {
|
|
281
281
|
const { callee } = node;
|
|
282
282
|
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
283
283
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
284
284
|
if (!variable) return;
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
if (!
|
|
285
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter || definition.type === TSESLint.Scope.DefinitionType.Variable);
|
|
286
|
+
if (!declarationDefinition) return;
|
|
287
|
+
const declarationNode = declarationDefinition.name;
|
|
288
|
+
if (declarationNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
289
|
+
if (!declarationHasReadonlyArrayConstituent(declarationNode)) return;
|
|
290
290
|
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
291
291
|
context.report({
|
|
292
292
|
node,
|
|
@@ -468,7 +468,7 @@ const noIndexFiles = {
|
|
|
468
468
|
};
|
|
469
469
|
//#endregion
|
|
470
470
|
//#region src/rules/no-map-instanceof-mutation.ts
|
|
471
|
-
const createRule$
|
|
471
|
+
const createRule$6 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
472
472
|
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
473
473
|
"set",
|
|
474
474
|
"delete",
|
|
@@ -485,33 +485,33 @@ function definitelyExits$1(statement) {
|
|
|
485
485
|
}
|
|
486
486
|
return false;
|
|
487
487
|
}
|
|
488
|
-
const noMapInstanceofMutation = createRule$
|
|
488
|
+
const noMapInstanceofMutation = createRule$6({
|
|
489
489
|
name: "no-map-instanceof-mutation",
|
|
490
490
|
meta: {
|
|
491
491
|
type: "problem",
|
|
492
492
|
schema: [],
|
|
493
|
-
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
494
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a
|
|
493
|
+
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
494
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a value whose real type includes ReadonlyMap (a caller's map, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
|
|
495
495
|
},
|
|
496
496
|
defaultOptions: [],
|
|
497
497
|
create(context) {
|
|
498
498
|
const services = ESLintUtils.getParserServices(context);
|
|
499
499
|
const checker = services.program.getTypeChecker();
|
|
500
|
-
function
|
|
501
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
502
|
-
const
|
|
503
|
-
return (
|
|
500
|
+
function declarationHasReadonlyMapConstituent(declarationNode) {
|
|
501
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
502
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
503
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
|
|
504
504
|
}
|
|
505
505
|
return { CallExpression(node) {
|
|
506
506
|
const { callee } = node;
|
|
507
507
|
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_MAP_METHODS.has(callee.property.name)) return;
|
|
508
508
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
509
509
|
if (!variable) return;
|
|
510
|
-
const
|
|
511
|
-
if (!
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
514
|
-
if (!
|
|
510
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter || definition.type === TSESLint.Scope.DefinitionType.Variable);
|
|
511
|
+
if (!declarationDefinition) return;
|
|
512
|
+
const declarationNode = declarationDefinition.name;
|
|
513
|
+
if (declarationNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
514
|
+
if (!declarationHasReadonlyMapConstituent(declarationNode)) return;
|
|
515
515
|
if (!isGuardedByInstanceofMap(node, variable, context)) return;
|
|
516
516
|
context.report({
|
|
517
517
|
node,
|
|
@@ -576,7 +576,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
576
576
|
"fill",
|
|
577
577
|
"copyWithin"
|
|
578
578
|
]);
|
|
579
|
-
const createRule$
|
|
579
|
+
const createRule$5 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
580
580
|
function isUnionArrayType(typeAnnotation) {
|
|
581
581
|
if (typeAnnotation.type === AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
582
582
|
if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "Array" && typeAnnotation.typeArguments?.params.length === 1) {
|
|
@@ -584,7 +584,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
584
584
|
if (firstParam?.type === AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
|
-
const noMutableUnionArrayParam = createRule$
|
|
587
|
+
const noMutableUnionArrayParam = createRule$5({
|
|
588
588
|
name: "no-mutable-union-array-param",
|
|
589
589
|
meta: {
|
|
590
590
|
type: "problem",
|
|
@@ -710,14 +710,14 @@ const noNonBarrelReexport = {
|
|
|
710
710
|
};
|
|
711
711
|
//#endregion
|
|
712
712
|
//#region src/rules/no-object-assign.ts
|
|
713
|
-
const createRule$
|
|
713
|
+
const createRule$4 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
714
714
|
function resolveFrom$1(scope, name) {
|
|
715
715
|
for (let current = scope; current; current = current.upper) {
|
|
716
716
|
const found = current.set.get(name);
|
|
717
717
|
if (found) return found;
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
|
-
const noObjectAssign = createRule$
|
|
720
|
+
const noObjectAssign = createRule$4({
|
|
721
721
|
name: "no-object-assign",
|
|
722
722
|
meta: {
|
|
723
723
|
type: "problem",
|
|
@@ -853,7 +853,7 @@ const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
|
853
853
|
"delete",
|
|
854
854
|
"clear"
|
|
855
855
|
]);
|
|
856
|
-
const createRule$
|
|
856
|
+
const createRule$3 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
857
857
|
function isSetInstanceofExpression(node) {
|
|
858
858
|
return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Set";
|
|
859
859
|
}
|
|
@@ -865,33 +865,33 @@ function definitelyExits(statement) {
|
|
|
865
865
|
}
|
|
866
866
|
return false;
|
|
867
867
|
}
|
|
868
|
-
const noSetInstanceofMutation = createRule$
|
|
868
|
+
const noSetInstanceofMutation = createRule$3({
|
|
869
869
|
name: "no-set-instanceof-mutation",
|
|
870
870
|
meta: {
|
|
871
871
|
type: "problem",
|
|
872
872
|
schema: [],
|
|
873
|
-
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
874
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a
|
|
873
|
+
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
874
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a value whose real type includes a ReadonlySet (a caller's set, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
|
|
875
875
|
},
|
|
876
876
|
defaultOptions: [],
|
|
877
877
|
create(context) {
|
|
878
878
|
const services = ESLintUtils.getParserServices(context);
|
|
879
879
|
const checker = services.program.getTypeChecker();
|
|
880
|
-
function
|
|
881
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
882
|
-
const
|
|
883
|
-
return (
|
|
880
|
+
function declarationHasReadonlySetConstituent(declarationNode) {
|
|
881
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
882
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
883
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
|
|
884
884
|
}
|
|
885
885
|
return { CallExpression(node) {
|
|
886
886
|
const { callee } = node;
|
|
887
887
|
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_SET_METHODS.has(callee.property.name)) return;
|
|
888
888
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
889
889
|
if (!variable) return;
|
|
890
|
-
const
|
|
891
|
-
if (!
|
|
892
|
-
const
|
|
893
|
-
if (
|
|
894
|
-
if (!
|
|
890
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter || definition.type === TSESLint.Scope.DefinitionType.Variable);
|
|
891
|
+
if (!declarationDefinition) return;
|
|
892
|
+
const declarationNode = declarationDefinition.name;
|
|
893
|
+
if (declarationNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
894
|
+
if (!declarationHasReadonlySetConstituent(declarationNode)) return;
|
|
895
895
|
if (!isGuardedBySetInstanceof(node, variable, context)) return;
|
|
896
896
|
context.report({
|
|
897
897
|
node,
|
|
@@ -968,13 +968,13 @@ const noSideEffectsInIndex = {
|
|
|
968
968
|
};
|
|
969
969
|
//#endregion
|
|
970
970
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
971
|
-
const createRule$
|
|
971
|
+
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
972
972
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
973
973
|
function isDefinitelyNumberType(type) {
|
|
974
974
|
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
975
975
|
return (type.flags & ts.TypeFlags.NumberLike) !== 0;
|
|
976
976
|
}
|
|
977
|
-
const preferNumericSortCompare = createRule$
|
|
977
|
+
const preferNumericSortCompare = createRule$2({
|
|
978
978
|
name: "prefer-numeric-sort-compare",
|
|
979
979
|
meta: {
|
|
980
980
|
type: "suggestion",
|
|
@@ -1020,7 +1020,7 @@ const preferNumericSortCompare = createRule$1({
|
|
|
1020
1020
|
});
|
|
1021
1021
|
//#endregion
|
|
1022
1022
|
//#region src/rules/prefer-readonly-array-param.ts
|
|
1023
|
-
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1023
|
+
const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1024
1024
|
function getFixableArrayOrTupleType(typeNode) {
|
|
1025
1025
|
if (typeNode.type === AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
|
|
1026
1026
|
if (typeNode.type === AST_NODE_TYPES.TSArrayType) return typeNode;
|
|
@@ -1032,12 +1032,12 @@ function getFixableTypesForAnnotation(typeNode) {
|
|
|
1032
1032
|
const fixable = getFixableArrayOrTupleType(typeNode);
|
|
1033
1033
|
return fixable ? [fixable] : [];
|
|
1034
1034
|
}
|
|
1035
|
-
function getAnnotatedParamNode(param) {
|
|
1036
|
-
if (param.type === AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(param.parameter);
|
|
1035
|
+
function getAnnotatedParamNode$1(param) {
|
|
1036
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode$1(param.parameter);
|
|
1037
1037
|
if (param.type === AST_NODE_TYPES.AssignmentPattern) return param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0;
|
|
1038
1038
|
if (param.type === AST_NODE_TYPES.RestElement || param.type === AST_NODE_TYPES.Identifier) return param;
|
|
1039
1039
|
}
|
|
1040
|
-
const FUNCTION_LIKE_SELECTOR = [
|
|
1040
|
+
const FUNCTION_LIKE_SELECTOR$1 = [
|
|
1041
1041
|
"ArrowFunctionExpression",
|
|
1042
1042
|
"FunctionDeclaration",
|
|
1043
1043
|
"FunctionExpression",
|
|
@@ -1048,7 +1048,7 @@ const FUNCTION_LIKE_SELECTOR = [
|
|
|
1048
1048
|
"TSFunctionType",
|
|
1049
1049
|
"TSMethodSignature"
|
|
1050
1050
|
].join(", ");
|
|
1051
|
-
const preferReadonlyArrayParam = createRule({
|
|
1051
|
+
const preferReadonlyArrayParam = createRule$1({
|
|
1052
1052
|
name: "prefer-readonly-array-param",
|
|
1053
1053
|
meta: {
|
|
1054
1054
|
type: "problem",
|
|
@@ -1060,7 +1060,7 @@ const preferReadonlyArrayParam = createRule({
|
|
|
1060
1060
|
defaultOptions: [],
|
|
1061
1061
|
create(context) {
|
|
1062
1062
|
function checkParam(param) {
|
|
1063
|
-
const annotatedNode = getAnnotatedParamNode(param);
|
|
1063
|
+
const annotatedNode = getAnnotatedParamNode$1(param);
|
|
1064
1064
|
if (!annotatedNode?.typeAnnotation) return;
|
|
1065
1065
|
const fixableTypes = getFixableTypesForAnnotation(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1066
1066
|
if (fixableTypes.length === 0) return;
|
|
@@ -1074,6 +1074,91 @@ const preferReadonlyArrayParam = createRule({
|
|
|
1074
1074
|
}
|
|
1075
1075
|
});
|
|
1076
1076
|
}
|
|
1077
|
+
return { [FUNCTION_LIKE_SELECTOR$1](node) {
|
|
1078
|
+
for (const param of node.params) checkParam(param);
|
|
1079
|
+
} };
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/rules/prefer-readonly-object-param.ts
|
|
1084
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1085
|
+
function getAnnotatedParamNode(param) {
|
|
1086
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(param.parameter);
|
|
1087
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern) return param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0;
|
|
1088
|
+
if (param.type === AST_NODE_TYPES.RestElement || param.type === AST_NODE_TYPES.Identifier) return param;
|
|
1089
|
+
}
|
|
1090
|
+
const FUNCTION_LIKE_SELECTOR = [
|
|
1091
|
+
"ArrowFunctionExpression",
|
|
1092
|
+
"FunctionDeclaration",
|
|
1093
|
+
"FunctionExpression",
|
|
1094
|
+
"TSCallSignatureDeclaration",
|
|
1095
|
+
"TSConstructSignatureDeclaration",
|
|
1096
|
+
"TSDeclareFunction",
|
|
1097
|
+
"TSEmptyBodyFunctionExpression",
|
|
1098
|
+
"TSFunctionType",
|
|
1099
|
+
"TSMethodSignature"
|
|
1100
|
+
].join(", ");
|
|
1101
|
+
function getCandidateTypeNode(typeNode) {
|
|
1102
|
+
if (typeNode.type === AST_NODE_TYPES.TSTypeLiteral) return typeNode;
|
|
1103
|
+
if (typeNode.type !== AST_NODE_TYPES.TSTypeReference) return void 0;
|
|
1104
|
+
if (typeNode.typeName.type !== AST_NODE_TYPES.Identifier) return void 0;
|
|
1105
|
+
if (typeNode.typeName.name === "Readonly") return void 0;
|
|
1106
|
+
return typeNode;
|
|
1107
|
+
}
|
|
1108
|
+
const PRIMITIVE_LIKE_FLAGS = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined;
|
|
1109
|
+
function isFlatPropertyType(checker, type) {
|
|
1110
|
+
if (type.isUnion()) return type.types.every((constituent) => isFlatPropertyType(checker, constituent));
|
|
1111
|
+
if ((type.flags & PRIMITIVE_LIKE_FLAGS) !== 0) return true;
|
|
1112
|
+
return checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 && checker.getPropertiesOfType(type).length === 0 && checker.getIndexInfosOfType(type).length === 0;
|
|
1113
|
+
}
|
|
1114
|
+
function isFlatObjectType(checker, type, location) {
|
|
1115
|
+
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection | ts.TypeFlags.TypeParameter)) return false;
|
|
1116
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
|
1117
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) return false;
|
|
1118
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0) return false;
|
|
1119
|
+
if ((type.getSymbol()?.flags ?? 0) & ts.SymbolFlags.Class) return false;
|
|
1120
|
+
const symbolName = type.getSymbol()?.name;
|
|
1121
|
+
if (symbolName === "Map" || symbolName === "ReadonlyMap" || symbolName === "Set" || symbolName === "ReadonlySet") return false;
|
|
1122
|
+
for (const property of checker.getPropertiesOfType(type)) if (!isFlatPropertyType(checker, checker.getTypeOfSymbolAtLocation(property, location))) return false;
|
|
1123
|
+
for (const indexInfo of checker.getIndexInfosOfType(type)) if (!isFlatPropertyType(checker, indexInfo.type)) return false;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
function isAlreadyFullyReadonly(checker, type) {
|
|
1127
|
+
const properties = checker.getPropertiesOfType(type);
|
|
1128
|
+
const indexInfos = checker.getIndexInfosOfType(type);
|
|
1129
|
+
if (properties.length === 0 && indexInfos.length === 0) return false;
|
|
1130
|
+
return properties.every((property) => isPropertyReadonlyInType(type, property.getEscapedName(), checker)) && indexInfos.every((indexInfo) => indexInfo.isReadonly);
|
|
1131
|
+
}
|
|
1132
|
+
const preferReadonlyObjectParam = createRule({
|
|
1133
|
+
name: "prefer-readonly-object-param",
|
|
1134
|
+
meta: {
|
|
1135
|
+
type: "problem",
|
|
1136
|
+
fixable: "code",
|
|
1137
|
+
docs: { description: "Require a 'flat' object parameter (every property is a primitive or a callback, so a shallow wrapper is provably sufficient) to be typed 'Readonly<T>' -- a narrower, safely-autofixable sibling of @typescript-eslint/prefer-readonly-parameter-types and of this package's own prefer-readonly-array-param, scoped to the object shapes where a shallow fix is genuinely complete." },
|
|
1138
|
+
schema: [],
|
|
1139
|
+
messages: { preferReadonlyObject: "This object parameter is 'flat' -- every property (and index-signature value, if any) is a primitive or a callback, so there is no nested mutable state a shallow wrapper could miss. Wrap it in 'Readonly<...>' so a caller can pass a readonly or shared object with confidence, and so any attempt to mutate it inside the function becomes a deliberate, visible compile error instead of a silent side effect on the caller's data." }
|
|
1140
|
+
},
|
|
1141
|
+
defaultOptions: [],
|
|
1142
|
+
create(context) {
|
|
1143
|
+
const services = ESLintUtils.getParserServices(context);
|
|
1144
|
+
const checker = services.program.getTypeChecker();
|
|
1145
|
+
function checkParam(param) {
|
|
1146
|
+
const annotatedNode = getAnnotatedParamNode(param);
|
|
1147
|
+
if (!annotatedNode?.typeAnnotation) return;
|
|
1148
|
+
const candidateTypeNode = getCandidateTypeNode(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1149
|
+
if (!candidateTypeNode) return;
|
|
1150
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(annotatedNode);
|
|
1151
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
1152
|
+
if (!isFlatObjectType(checker, type, tsNode)) return;
|
|
1153
|
+
if (isAlreadyFullyReadonly(checker, type)) return;
|
|
1154
|
+
context.report({
|
|
1155
|
+
node: param,
|
|
1156
|
+
messageId: "preferReadonlyObject",
|
|
1157
|
+
fix(fixer) {
|
|
1158
|
+
return [fixer.insertTextBefore(candidateTypeNode, "Readonly<"), fixer.insertTextAfter(candidateTypeNode, ">")];
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1077
1162
|
return { [FUNCTION_LIKE_SELECTOR](node) {
|
|
1078
1163
|
for (const param of node.params) checkParam(param);
|
|
1079
1164
|
} };
|
|
@@ -1103,7 +1188,8 @@ const plugin = {
|
|
|
1103
1188
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1104
1189
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1105
1190
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1106
|
-
"prefer-readonly-array-param": preferReadonlyArrayParam
|
|
1191
|
+
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1192
|
+
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
1107
1193
|
},
|
|
1108
1194
|
configs: {
|
|
1109
1195
|
get recommended() {
|
|
@@ -1148,6 +1234,7 @@ const recommendedTypeChecked = [
|
|
|
1148
1234
|
"exadev/no-set-instanceof-mutation": "error",
|
|
1149
1235
|
"exadev/prefer-numeric-sort-compare": "error",
|
|
1150
1236
|
"exadev/prefer-readonly-array-param": "error",
|
|
1237
|
+
"exadev/prefer-readonly-object-param": "error",
|
|
1151
1238
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
1152
1239
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1153
1240
|
"@typescript-eslint/consistent-type-exports": "error",
|