@exadev/eslint-config 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- | `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 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, neither visible from the parameter's own syntax alone. |
119
- | `no-map-instanceof-mutation` | | `Map` is declared as extending `ReadonlyMap`, so `instanceof Map` narrows a parameter 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`. |
120
- | `no-set-instanceof-mutation` | | `instanceof Set` narrows a parameter 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`. |
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.5.0";
35
+ var version = "2.6.1";
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$6 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$6({
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 caller's genuinely readonly array can be mutated here even though the parameter's real type includes a readonly array. 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." }
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 parameterHasReadonlyArrayConstituent(parameterNode) {
304
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
305
- const parameterType = checker.getTypeAtLocation(tsNode);
306
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
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 parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
314
- if (!parameterDefinition) return;
315
- const parameterNode = parameterDefinition.name;
316
- if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
317
- if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
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$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$5({
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 caller's genuinely read-only ReadonlyMap can be mutated here even though the parameter's real type includes ReadonlyMap. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
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 parameterHasReadonlyMapConstituent(parameterNode) {
529
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
530
- const parameterType = checker.getTypeAtLocation(tsNode);
531
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
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 parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
539
- if (!parameterDefinition) return;
540
- const parameterNode = parameterDefinition.name;
541
- if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
542
- if (!parameterHasReadonlyMapConstituent(parameterNode)) return;
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$4 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$4({
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$3 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$3({
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$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$2({
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 caller's genuinely read-only set can be mutated here even though the parameter's real type includes a ReadonlySet. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
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 parameterHasReadonlySetConstituent(parameterNode) {
909
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
910
- const parameterType = checker.getTypeAtLocation(tsNode);
911
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
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 parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
919
- if (!parameterDefinition) return;
920
- const parameterNode = parameterDefinition.name;
921
- if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
922
- if (!parameterHasReadonlySetConstituent(parameterNode)) return;
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$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$1({
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.5.0";
7
+ var version = "2.6.1";
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$6 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$6({
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 caller's genuinely readonly array can be mutated here even though the parameter's real type includes a readonly array. 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." }
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 parameterHasReadonlyArrayConstituent(parameterNode) {
276
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
277
- const parameterType = checker.getTypeAtLocation(tsNode);
278
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
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 parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
286
- if (!parameterDefinition) return;
287
- const parameterNode = parameterDefinition.name;
288
- if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
289
- if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
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$5 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$5({
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 caller's genuinely read-only ReadonlyMap can be mutated here even though the parameter's real type includes ReadonlyMap. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
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 parameterHasReadonlyMapConstituent(parameterNode) {
501
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
502
- const parameterType = checker.getTypeAtLocation(tsNode);
503
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
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 parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
511
- if (!parameterDefinition) return;
512
- const parameterNode = parameterDefinition.name;
513
- if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
514
- if (!parameterHasReadonlyMapConstituent(parameterNode)) return;
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$4 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$4({
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$3 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$3({
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$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$2({
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 caller's genuinely read-only set can be mutated here even though the parameter's real type includes a ReadonlySet. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
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 parameterHasReadonlySetConstituent(parameterNode) {
881
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
882
- const parameterType = checker.getTypeAtLocation(tsNode);
883
- return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
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 parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
891
- if (!parameterDefinition) return;
892
- const parameterNode = parameterDefinition.name;
893
- if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
894
- if (!parameterHasReadonlySetConstituent(parameterNode)) return;
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$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
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$1({
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exadev/eslint-config",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Shared custom ESLint rules and plugin for ExaDev projects",
5
5
  "type": "module",
6
6
  "sideEffects": false,