@exadev/eslint-config 2.4.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/dist/index.js CHANGED
@@ -2,8 +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 { isPropertyReadonlyInType, isTypeReference } from "ts-api-utils";
5
6
  //#region package.json
6
- var version = "2.4.0";
7
+ var version = "2.6.0";
7
8
  //#endregion
8
9
  //#region src/rules/barrel-helpers.ts
9
10
  const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
@@ -247,45 +248,45 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
247
248
  "fill",
248
249
  "copyWithin"
249
250
  ]);
250
- const createRule$2 = 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`);
251
252
  function isArrayIsArrayCall(node) {
252
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";
253
254
  }
254
- function definitelyExits(statement) {
255
+ function definitelyExits$2(statement) {
255
256
  if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
256
257
  if (statement.type === AST_NODE_TYPES.BlockStatement) {
257
258
  const last = statement.body.at(-1);
258
- return last !== void 0 && definitelyExits(last);
259
+ return last !== void 0 && definitelyExits$2(last);
259
260
  }
260
261
  return false;
261
262
  }
262
- const noArrayIsArrayMutation = createRule$2({
263
+ const noArrayIsArrayMutation = createRule$7({
263
264
  name: "no-array-isarray-mutation",
264
265
  meta: {
265
266
  type: "problem",
266
267
  schema: [],
267
- 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." },
268
- 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." }
269
270
  },
270
271
  defaultOptions: [],
271
272
  create(context) {
272
273
  const services = ESLintUtils.getParserServices(context);
273
274
  const checker = services.program.getTypeChecker();
274
- function parameterHasReadonlyArrayConstituent(parameterNode) {
275
- const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
276
- const parameterType = checker.getTypeAtLocation(tsNode);
277
- 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");
278
279
  }
279
280
  return { CallExpression(node) {
280
281
  const { callee } = node;
281
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;
282
283
  const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
283
284
  if (!variable) return;
284
- const parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
285
- if (!parameterDefinition) return;
286
- const parameterNode = parameterDefinition.name;
287
- if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
288
- 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;
289
290
  if (!isGuardedByArrayIsArray(node, variable, context)) return;
290
291
  context.report({
291
292
  node,
@@ -332,7 +333,7 @@ const noArrayIsArrayMutation = createRule$2({
332
333
  }
333
334
  if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
334
335
  const sibling = statements[i];
335
- if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
336
+ if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent)) return true;
336
337
  }
337
338
  }
338
339
  current = parent;
@@ -466,6 +467,107 @@ const noIndexFiles = {
466
467
  }
467
468
  };
468
469
  //#endregion
470
+ //#region src/rules/no-map-instanceof-mutation.ts
471
+ const createRule$6 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
472
+ const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
473
+ "set",
474
+ "delete",
475
+ "clear"
476
+ ]);
477
+ function isInstanceofMapExpression(node) {
478
+ return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Map";
479
+ }
480
+ function definitelyExits$1(statement) {
481
+ if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
482
+ if (statement.type === AST_NODE_TYPES.BlockStatement) {
483
+ const last = statement.body.at(-1);
484
+ return last !== void 0 && definitelyExits$1(last);
485
+ }
486
+ return false;
487
+ }
488
+ const noMapInstanceofMutation = createRule$6({
489
+ name: "no-map-instanceof-mutation",
490
+ meta: {
491
+ type: "problem",
492
+ schema: [],
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
+ },
496
+ defaultOptions: [],
497
+ create(context) {
498
+ const services = ESLintUtils.getParserServices(context);
499
+ const checker = services.program.getTypeChecker();
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
+ }
505
+ return { CallExpression(node) {
506
+ const { callee } = node;
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
+ const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
509
+ if (!variable) 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
+ if (!isGuardedByInstanceofMap(node, variable, context)) return;
516
+ context.report({
517
+ node,
518
+ messageId: "unsound",
519
+ data: { method: callee.property.name }
520
+ });
521
+ } };
522
+ function resolvesToVariable(identifier, target, atNode, ruleContext) {
523
+ return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
524
+ }
525
+ function isNegatedInstanceofMapExpression(testNode, target, ruleContext) {
526
+ if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
527
+ return matchesInstanceofMapOn(testNode.argument, target, ruleContext);
528
+ }
529
+ function matchesInstanceofMapOn(testNode, target, ruleContext) {
530
+ if (!isInstanceofMapExpression(testNode)) return false;
531
+ const { left } = testNode;
532
+ return left.type === AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
533
+ }
534
+ function isGuardedByInstanceofMap(startNode, parameterVariable, ruleContext) {
535
+ let current = startNode;
536
+ while (current.parent) {
537
+ const { parent } = current;
538
+ if (parent.type === AST_NODE_TYPES.IfStatement) {
539
+ if (parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
540
+ if (parent.alternate === current && isNegatedInstanceofMapExpression(parent.test, parameterVariable, ruleContext)) return true;
541
+ }
542
+ if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesInstanceofMapOn(parent.left, parameterVariable, ruleContext)) return true;
543
+ if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
544
+ current = parent;
545
+ }
546
+ return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
547
+ }
548
+ function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
549
+ let current = startNode;
550
+ while (current.parent) {
551
+ const { parent } = current;
552
+ if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
553
+ const statements = parent.body;
554
+ let ownIndex = -1;
555
+ for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
556
+ ownIndex = i;
557
+ break;
558
+ }
559
+ if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
560
+ const sibling = statements[i];
561
+ if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedInstanceofMapExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits$1(sibling.consequent)) return true;
562
+ }
563
+ }
564
+ current = parent;
565
+ }
566
+ return false;
567
+ }
568
+ }
569
+ });
570
+ //#endregion
469
571
  //#region src/rules/no-mutable-union-array-param.ts
470
572
  const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
471
573
  "push",
@@ -474,7 +576,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
474
576
  "fill",
475
577
  "copyWithin"
476
578
  ]);
477
- const createRule$1 = 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`);
478
580
  function isUnionArrayType(typeAnnotation) {
479
581
  if (typeAnnotation.type === AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
480
582
  if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "Array" && typeAnnotation.typeArguments?.params.length === 1) {
@@ -482,7 +584,7 @@ function isUnionArrayType(typeAnnotation) {
482
584
  if (firstParam?.type === AST_NODE_TYPES.TSUnionType) return firstParam;
483
585
  }
484
586
  }
485
- const noMutableUnionArrayParam = createRule$1({
587
+ const noMutableUnionArrayParam = createRule$5({
486
588
  name: "no-mutable-union-array-param",
487
589
  meta: {
488
590
  type: "problem",
@@ -608,14 +710,14 @@ const noNonBarrelReexport = {
608
710
  };
609
711
  //#endregion
610
712
  //#region src/rules/no-object-assign.ts
611
- const createRule = 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`);
612
714
  function resolveFrom$1(scope, name) {
613
715
  for (let current = scope; current; current = current.upper) {
614
716
  const found = current.set.get(name);
615
717
  if (found) return found;
616
718
  }
617
719
  }
618
- const noObjectAssign = createRule({
720
+ const noObjectAssign = createRule$4({
619
721
  name: "no-object-assign",
620
722
  meta: {
621
723
  type: "problem",
@@ -690,6 +792,378 @@ function resolveFrom(scope, name) {
690
792
  if (found) return found;
691
793
  }
692
794
  }
795
+ const noPointlessReassignment = {
796
+ meta: {
797
+ type: "problem",
798
+ fixable: "code",
799
+ schema: [],
800
+ messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
801
+ },
802
+ create(context) {
803
+ return { VariableDeclarator(node) {
804
+ if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
805
+ if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
806
+ const scope = context.sourceCode.getScope(node);
807
+ const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
808
+ if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
809
+ const aliasName = node.id.name;
810
+ const originalName = node.init.name;
811
+ const aliasIsAnnotated = hasTypeAnnotation(node.id);
812
+ context.report({
813
+ node,
814
+ messageId: "pointlessReassignment",
815
+ data: {
816
+ name: aliasName,
817
+ value: originalName
818
+ },
819
+ fix(fixer) {
820
+ const variable = scope.set.get(aliasName);
821
+ if (!variable) return null;
822
+ if (aliasIsAnnotated) return null;
823
+ if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
824
+ const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
825
+ if (readRefs.some((reference) => {
826
+ const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
827
+ if (afterToken?.value === ":") return false;
828
+ if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
829
+ let token = context.sourceCode.getTokenBefore(reference.identifier);
830
+ while (token) {
831
+ if (token.value === "{") return true;
832
+ if (token.value === "[" || token.value === "(") return false;
833
+ if (token.value === ":") return false;
834
+ token = context.sourceCode.getTokenBefore(token);
835
+ }
836
+ return false;
837
+ })) return null;
838
+ if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
839
+ const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
840
+ const declaration = node.parent;
841
+ if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
842
+ fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
843
+ return fixes;
844
+ }
845
+ });
846
+ } };
847
+ }
848
+ };
849
+ //#endregion
850
+ //#region src/rules/no-set-instanceof-mutation.ts
851
+ const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
852
+ "add",
853
+ "delete",
854
+ "clear"
855
+ ]);
856
+ const createRule$3 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
857
+ function isSetInstanceofExpression(node) {
858
+ return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Set";
859
+ }
860
+ function definitelyExits(statement) {
861
+ if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
862
+ if (statement.type === AST_NODE_TYPES.BlockStatement) {
863
+ const last = statement.body.at(-1);
864
+ return last !== void 0 && definitelyExits(last);
865
+ }
866
+ return false;
867
+ }
868
+ const noSetInstanceofMutation = createRule$3({
869
+ name: "no-set-instanceof-mutation",
870
+ meta: {
871
+ type: "problem",
872
+ schema: [],
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
+ },
876
+ defaultOptions: [],
877
+ create(context) {
878
+ const services = ESLintUtils.getParserServices(context);
879
+ const checker = services.program.getTypeChecker();
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
+ }
885
+ return { CallExpression(node) {
886
+ const { callee } = node;
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
+ const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
889
+ if (!variable) 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
+ if (!isGuardedBySetInstanceof(node, variable, context)) return;
896
+ context.report({
897
+ node,
898
+ messageId: "unsound",
899
+ data: { method: callee.property.name }
900
+ });
901
+ } };
902
+ function resolvesToVariable(identifier, target, atNode, ruleContext) {
903
+ return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
904
+ }
905
+ function isNegatedSetInstanceofExpression(testNode, target, ruleContext) {
906
+ if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
907
+ return matchesSetInstanceofOn(testNode.argument, target, ruleContext);
908
+ }
909
+ function matchesSetInstanceofOn(testNode, target, ruleContext) {
910
+ if (!isSetInstanceofExpression(testNode)) return false;
911
+ const { left } = testNode;
912
+ return left.type === AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
913
+ }
914
+ function isGuardedBySetInstanceof(startNode, parameterVariable, ruleContext) {
915
+ let current = startNode;
916
+ while (current.parent) {
917
+ const { parent } = current;
918
+ if (parent.type === AST_NODE_TYPES.IfStatement) {
919
+ if (parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
920
+ if (parent.alternate === current && isNegatedSetInstanceofExpression(parent.test, parameterVariable, ruleContext)) return true;
921
+ }
922
+ if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesSetInstanceofOn(parent.left, parameterVariable, ruleContext)) return true;
923
+ if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
924
+ current = parent;
925
+ }
926
+ return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
927
+ }
928
+ function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
929
+ let current = startNode;
930
+ while (current.parent) {
931
+ const { parent } = current;
932
+ if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
933
+ const statements = parent.body;
934
+ let ownIndex = -1;
935
+ for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
936
+ ownIndex = i;
937
+ break;
938
+ }
939
+ if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
940
+ const sibling = statements[i];
941
+ if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedSetInstanceofExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
942
+ }
943
+ }
944
+ current = parent;
945
+ }
946
+ return false;
947
+ }
948
+ }
949
+ });
950
+ //#endregion
951
+ //#region src/rules/no-side-effects-in-index.ts
952
+ const noSideEffectsInIndex = {
953
+ meta: {
954
+ type: "problem",
955
+ schema: [],
956
+ messages: { notAPureReexport: "A barrel (index) file may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...') -- nothing else, so it can never have a side effect at import time by construction. Found: {{ description }}." }
957
+ },
958
+ create(context) {
959
+ if (!isIndexFile(context.filename)) return {};
960
+ return { Program(node) {
961
+ for (const statement of node.body) if (!isPureReexport(statement)) context.report({
962
+ node: statement,
963
+ messageId: "notAPureReexport",
964
+ data: { description: statement.type }
965
+ });
966
+ } };
967
+ }
968
+ };
969
+ //#endregion
970
+ //#region src/rules/prefer-numeric-sort-compare.ts
971
+ const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
972
+ const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
973
+ function isDefinitelyNumberType(type) {
974
+ if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
975
+ return (type.flags & ts.TypeFlags.NumberLike) !== 0;
976
+ }
977
+ const preferNumericSortCompare = createRule$2({
978
+ name: "prefer-numeric-sort-compare",
979
+ meta: {
980
+ type: "suggestion",
981
+ hasSuggestions: true,
982
+ docs: { description: "Suggest an ascending numeric compare function for a bare '.sort()'/'.toSorted()' call on an array whose element type is definitively 'number' -- the default comparator sorts lexicographically, so a bare numeric sort is essentially always a bug." },
983
+ schema: [],
984
+ messages: {
985
+ preferNumericCompare: "'.{{ method }}()' on a number array with no compare function sorts lexicographically (e.g. [1, 2, 10].sort() becomes [1, 10, 2]), not in ascending numeric order. Provide a compare function.",
986
+ addAscendingCompare: "Add an ascending numeric compare function: '(a, b) => a - b'."
987
+ }
988
+ },
989
+ defaultOptions: [],
990
+ create(context) {
991
+ const services = ESLintUtils.getParserServices(context);
992
+ const checker = services.program.getTypeChecker();
993
+ return { CallExpression(node) {
994
+ if (node.arguments.length > 0) return;
995
+ const { callee } = node;
996
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return;
997
+ if (callee.property.type !== AST_NODE_TYPES.Identifier || !SORT_METHOD_NAMES.has(callee.property.name)) return;
998
+ const receiverTsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
999
+ if (!ts.isExpression(receiverTsNode)) return;
1000
+ const receiverType = checker.getTypeAtLocation(receiverTsNode);
1001
+ if (!checker.isArrayType(receiverType)) return;
1002
+ if (!isTypeReference(receiverType)) return;
1003
+ const [elementType] = checker.getTypeArguments(receiverType);
1004
+ if (!elementType || !isDefinitelyNumberType(elementType)) return;
1005
+ context.report({
1006
+ node,
1007
+ messageId: "preferNumericCompare",
1008
+ data: { method: callee.property.name },
1009
+ suggest: [{
1010
+ messageId: "addAscendingCompare",
1011
+ fix(fixer) {
1012
+ const closingParen = context.sourceCode.getLastToken(node);
1013
+ if (!closingParen) return null;
1014
+ return fixer.insertTextBefore(closingParen, "(a, b) => a - b");
1015
+ }
1016
+ }]
1017
+ });
1018
+ } };
1019
+ }
1020
+ });
1021
+ //#endregion
1022
+ //#region src/rules/prefer-readonly-array-param.ts
1023
+ const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
1024
+ function getFixableArrayOrTupleType(typeNode) {
1025
+ if (typeNode.type === AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
1026
+ if (typeNode.type === AST_NODE_TYPES.TSArrayType) return typeNode;
1027
+ if (typeNode.type === AST_NODE_TYPES.TSTupleType) return typeNode;
1028
+ if (typeNode.type === AST_NODE_TYPES.TSTypeReference && typeNode.typeName.type === AST_NODE_TYPES.Identifier && typeNode.typeName.name === "Array") return typeNode;
1029
+ }
1030
+ function getFixableTypesForAnnotation(typeNode) {
1031
+ if (typeNode.type === AST_NODE_TYPES.TSUnionType) return typeNode.types.flatMap(getFixableTypesForAnnotation);
1032
+ const fixable = getFixableArrayOrTupleType(typeNode);
1033
+ return fixable ? [fixable] : [];
1034
+ }
1035
+ function getAnnotatedParamNode$1(param) {
1036
+ if (param.type === AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode$1(param.parameter);
1037
+ if (param.type === AST_NODE_TYPES.AssignmentPattern) return param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0;
1038
+ if (param.type === AST_NODE_TYPES.RestElement || param.type === AST_NODE_TYPES.Identifier) return param;
1039
+ }
1040
+ const FUNCTION_LIKE_SELECTOR$1 = [
1041
+ "ArrowFunctionExpression",
1042
+ "FunctionDeclaration",
1043
+ "FunctionExpression",
1044
+ "TSCallSignatureDeclaration",
1045
+ "TSConstructSignatureDeclaration",
1046
+ "TSDeclareFunction",
1047
+ "TSEmptyBodyFunctionExpression",
1048
+ "TSFunctionType",
1049
+ "TSMethodSignature"
1050
+ ].join(", ");
1051
+ const preferReadonlyArrayParam = createRule$1({
1052
+ name: "prefer-readonly-array-param",
1053
+ meta: {
1054
+ type: "problem",
1055
+ fixable: "code",
1056
+ docs: { description: "Require array and tuple parameters to be typed readonly, regardless of whether the function body mutates them -- a narrower, safely-autofixable sibling of @typescript-eslint/prefer-readonly-parameter-types scoped to array/tuple shapes only." },
1057
+ schema: [],
1058
+ messages: { preferReadonly: "Array and tuple parameters should be typed readonly ({{ suggestion }}) so a caller can pass a readonly or shared array with confidence, and so any mutation inside the function becomes a deliberate, visible compile error instead of a silent side effect on the caller's data." }
1059
+ },
1060
+ defaultOptions: [],
1061
+ create(context) {
1062
+ function checkParam(param) {
1063
+ const annotatedNode = getAnnotatedParamNode$1(param);
1064
+ if (!annotatedNode?.typeAnnotation) return;
1065
+ const fixableTypes = getFixableTypesForAnnotation(annotatedNode.typeAnnotation.typeAnnotation);
1066
+ if (fixableTypes.length === 0) return;
1067
+ const suggestion = fixableTypes.map((fixableType) => fixableType.type === AST_NODE_TYPES.TSTypeReference ? "ReadonlyArray<T>" : `readonly ${fixableType.type === AST_NODE_TYPES.TSTupleType ? "[T, U]" : "T[]"}`).join(" / ");
1068
+ context.report({
1069
+ node: param,
1070
+ messageId: "preferReadonly",
1071
+ data: { suggestion },
1072
+ fix(fixer) {
1073
+ return fixableTypes.map((fixableType) => fixableType.type === AST_NODE_TYPES.TSTypeReference ? fixer.replaceText(fixableType.typeName, "ReadonlyArray") : fixer.insertTextBefore(fixableType, "readonly "));
1074
+ }
1075
+ });
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
+ }
1162
+ return { [FUNCTION_LIKE_SELECTOR](node) {
1163
+ for (const param of node.params) checkParam(param);
1164
+ } };
1165
+ }
1166
+ });
693
1167
  //#endregion
694
1168
  //#region src/plugin.ts
695
1169
  const plugin = {
@@ -705,81 +1179,17 @@ const plugin = {
705
1179
  "no-enum-number-widening": noEnumNumberWidening,
706
1180
  "no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
707
1181
  "no-index-files": noIndexFiles,
1182
+ "no-map-instanceof-mutation": noMapInstanceofMutation,
708
1183
  "no-mutable-union-array-param": noMutableUnionArrayParam,
709
1184
  "no-non-barrel-index": noNonBarrelIndex,
710
1185
  "no-non-barrel-reexport": noNonBarrelReexport,
711
1186
  "no-object-assign": noObjectAssign,
712
- "no-pointless-reassignment": {
713
- meta: {
714
- type: "problem",
715
- fixable: "code",
716
- schema: [],
717
- messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
718
- },
719
- create(context) {
720
- return { VariableDeclarator(node) {
721
- if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
722
- if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
723
- const scope = context.sourceCode.getScope(node);
724
- const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
725
- if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
726
- const aliasName = node.id.name;
727
- const originalName = node.init.name;
728
- const aliasIsAnnotated = hasTypeAnnotation(node.id);
729
- context.report({
730
- node,
731
- messageId: "pointlessReassignment",
732
- data: {
733
- name: aliasName,
734
- value: originalName
735
- },
736
- fix(fixer) {
737
- const variable = scope.set.get(aliasName);
738
- if (!variable) return null;
739
- if (aliasIsAnnotated) return null;
740
- if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
741
- const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
742
- if (readRefs.some((reference) => {
743
- const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
744
- if (afterToken?.value === ":") return false;
745
- if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
746
- let token = context.sourceCode.getTokenBefore(reference.identifier);
747
- while (token) {
748
- if (token.value === "{") return true;
749
- if (token.value === "[" || token.value === "(") return false;
750
- if (token.value === ":") return false;
751
- token = context.sourceCode.getTokenBefore(token);
752
- }
753
- return false;
754
- })) return null;
755
- if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
756
- const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
757
- const declaration = node.parent;
758
- if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
759
- fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
760
- return fixes;
761
- }
762
- });
763
- } };
764
- }
765
- },
766
- "no-side-effects-in-index": {
767
- meta: {
768
- type: "problem",
769
- schema: [],
770
- messages: { notAPureReexport: "A barrel (index) file may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...') -- nothing else, so it can never have a side effect at import time by construction. Found: {{ description }}." }
771
- },
772
- create(context) {
773
- if (!isIndexFile(context.filename)) return {};
774
- return { Program(node) {
775
- for (const statement of node.body) if (!isPureReexport(statement)) context.report({
776
- node: statement,
777
- messageId: "notAPureReexport",
778
- data: { description: statement.type }
779
- });
780
- } };
781
- }
782
- }
1187
+ "no-pointless-reassignment": noPointlessReassignment,
1188
+ "no-set-instanceof-mutation": noSetInstanceofMutation,
1189
+ "no-side-effects-in-index": noSideEffectsInIndex,
1190
+ "prefer-numeric-sort-compare": preferNumericSortCompare,
1191
+ "prefer-readonly-array-param": preferReadonlyArrayParam,
1192
+ "prefer-readonly-object-param": preferReadonlyObjectParam
783
1193
  },
784
1194
  configs: {
785
1195
  get recommended() {
@@ -790,7 +1200,8 @@ const plugin = {
790
1200
  "exadev/barrel-policy": ["error", { mode: "banned" }],
791
1201
  "exadev/no-mutable-union-array-param": "error",
792
1202
  "exadev/no-object-assign": "error",
793
- "exadev/no-pointless-reassignment": "error"
1203
+ "exadev/no-pointless-reassignment": "error",
1204
+ "exadev/prefer-readonly-array-param": "error"
794
1205
  }
795
1206
  };
796
1207
  },
@@ -816,11 +1227,18 @@ const recommendedTypeChecked = [
816
1227
  "exadev/no-array-isarray-mutation": "error",
817
1228
  "exadev/no-enum-number-widening": "error",
818
1229
  "exadev/no-enum-reverse-lookup-widening": "error",
1230
+ "exadev/no-map-instanceof-mutation": "error",
819
1231
  "exadev/no-mutable-union-array-param": "error",
820
1232
  "exadev/no-object-assign": "error",
821
1233
  "exadev/no-pointless-reassignment": "error",
1234
+ "exadev/no-set-instanceof-mutation": "error",
1235
+ "exadev/prefer-numeric-sort-compare": "error",
1236
+ "exadev/prefer-readonly-array-param": "error",
1237
+ "exadev/prefer-readonly-object-param": "error",
822
1238
  "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
823
1239
  "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
1240
+ "@typescript-eslint/consistent-type-exports": "error",
1241
+ "@typescript-eslint/consistent-type-imports": "error",
824
1242
  "@typescript-eslint/method-signature-style": ["error", "property"],
825
1243
  "@typescript-eslint/no-deprecated": "error",
826
1244
  "@typescript-eslint/no-magic-numbers": ["error", {
@@ -840,6 +1258,7 @@ const recommendedTypeChecked = [
840
1258
  "@typescript-eslint/no-non-null-assertion": "error",
841
1259
  "@typescript-eslint/no-unnecessary-condition": "error",
842
1260
  "@typescript-eslint/prefer-readonly": "error",
1261
+ "@typescript-eslint/promise-function-async": "error",
843
1262
  "@typescript-eslint/require-array-sort-compare": "error",
844
1263
  "@typescript-eslint/strict-boolean-expressions": "error",
845
1264
  "@typescript-eslint/switch-exhaustiveness-check": "error",