@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/README.md +7 -2
- package/dist/index.cjs +512 -93
- package/dist/index.js +512 -93
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -30,8 +30,9 @@ let node_path = require("node:path");
|
|
|
30
30
|
let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
31
31
|
let typescript = require("typescript");
|
|
32
32
|
typescript = __toESM(typescript, 1);
|
|
33
|
+
let ts_api_utils = require("ts-api-utils");
|
|
33
34
|
//#region package.json
|
|
34
|
-
var version = "2.
|
|
35
|
+
var version = "2.6.0";
|
|
35
36
|
//#endregion
|
|
36
37
|
//#region src/rules/barrel-helpers.ts
|
|
37
38
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -275,45 +276,45 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
|
275
276
|
"fill",
|
|
276
277
|
"copyWithin"
|
|
277
278
|
]);
|
|
278
|
-
const createRule$
|
|
279
|
+
const createRule$7 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
279
280
|
function isArrayIsArrayCall(node) {
|
|
280
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";
|
|
281
282
|
}
|
|
282
|
-
function definitelyExits(statement) {
|
|
283
|
+
function definitelyExits$2(statement) {
|
|
283
284
|
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ThrowStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ContinueStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BreakStatement) return true;
|
|
284
285
|
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) {
|
|
285
286
|
const last = statement.body.at(-1);
|
|
286
|
-
return last !== void 0 && definitelyExits(last);
|
|
287
|
+
return last !== void 0 && definitelyExits$2(last);
|
|
287
288
|
}
|
|
288
289
|
return false;
|
|
289
290
|
}
|
|
290
|
-
const noArrayIsArrayMutation = createRule$
|
|
291
|
+
const noArrayIsArrayMutation = createRule$7({
|
|
291
292
|
name: "no-array-isarray-mutation",
|
|
292
293
|
meta: {
|
|
293
294
|
type: "problem",
|
|
294
295
|
schema: [],
|
|
295
|
-
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." },
|
|
296
|
-
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a
|
|
296
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter or local variable whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
297
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a value whose real type includes a readonly array (a caller's array, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
297
298
|
},
|
|
298
299
|
defaultOptions: [],
|
|
299
300
|
create(context) {
|
|
300
301
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
301
302
|
const checker = services.program.getTypeChecker();
|
|
302
|
-
function
|
|
303
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(
|
|
304
|
-
const
|
|
305
|
-
return (
|
|
303
|
+
function declarationHasReadonlyArrayConstituent(declarationNode) {
|
|
304
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(declarationNode);
|
|
305
|
+
const declaredType = checker.getTypeAtLocation(tsNode);
|
|
306
|
+
return (declaredType.isUnion() ? declaredType.types : [declaredType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
306
307
|
}
|
|
307
308
|
return { CallExpression(node) {
|
|
308
309
|
const { callee } = node;
|
|
309
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;
|
|
310
311
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
311
312
|
if (!variable) return;
|
|
312
|
-
const
|
|
313
|
-
if (!
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
if (!
|
|
313
|
+
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
314
|
+
if (!declarationDefinition) return;
|
|
315
|
+
const declarationNode = declarationDefinition.name;
|
|
316
|
+
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
317
|
+
if (!declarationHasReadonlyArrayConstituent(declarationNode)) return;
|
|
317
318
|
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
318
319
|
context.report({
|
|
319
320
|
node,
|
|
@@ -360,7 +361,7 @@ const noArrayIsArrayMutation = createRule$2({
|
|
|
360
361
|
}
|
|
361
362
|
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
362
363
|
const sibling = statements[i];
|
|
363
|
-
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
364
|
+
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent)) return true;
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
367
|
current = parent;
|
|
@@ -494,6 +495,107 @@ const noIndexFiles = {
|
|
|
494
495
|
}
|
|
495
496
|
};
|
|
496
497
|
//#endregion
|
|
498
|
+
//#region src/rules/no-map-instanceof-mutation.ts
|
|
499
|
+
const createRule$6 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
500
|
+
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
501
|
+
"set",
|
|
502
|
+
"delete",
|
|
503
|
+
"clear"
|
|
504
|
+
]);
|
|
505
|
+
function isInstanceofMapExpression(node) {
|
|
506
|
+
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 === "Map";
|
|
507
|
+
}
|
|
508
|
+
function definitelyExits$1(statement) {
|
|
509
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ThrowStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ContinueStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BreakStatement) return true;
|
|
510
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) {
|
|
511
|
+
const last = statement.body.at(-1);
|
|
512
|
+
return last !== void 0 && definitelyExits$1(last);
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
const noMapInstanceofMutation = createRule$6({
|
|
517
|
+
name: "no-map-instanceof-mutation",
|
|
518
|
+
meta: {
|
|
519
|
+
type: "problem",
|
|
520
|
+
schema: [],
|
|
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
|
+
},
|
|
524
|
+
defaultOptions: [],
|
|
525
|
+
create(context) {
|
|
526
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
527
|
+
const checker = services.program.getTypeChecker();
|
|
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
|
+
}
|
|
533
|
+
return { CallExpression(node) {
|
|
534
|
+
const { callee } = node;
|
|
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
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
537
|
+
if (!variable) 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
|
+
if (!isGuardedByInstanceofMap(node, variable, context)) return;
|
|
544
|
+
context.report({
|
|
545
|
+
node,
|
|
546
|
+
messageId: "unsound",
|
|
547
|
+
data: { method: callee.property.name }
|
|
548
|
+
});
|
|
549
|
+
} };
|
|
550
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
551
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
552
|
+
}
|
|
553
|
+
function isNegatedInstanceofMapExpression(testNode, target, ruleContext) {
|
|
554
|
+
if (testNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
555
|
+
return matchesInstanceofMapOn(testNode.argument, target, ruleContext);
|
|
556
|
+
}
|
|
557
|
+
function matchesInstanceofMapOn(testNode, target, ruleContext) {
|
|
558
|
+
if (!isInstanceofMapExpression(testNode)) return false;
|
|
559
|
+
const { left } = testNode;
|
|
560
|
+
return left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
|
|
561
|
+
}
|
|
562
|
+
function isGuardedByInstanceofMap(startNode, parameterVariable, ruleContext) {
|
|
563
|
+
let current = startNode;
|
|
564
|
+
while (current.parent) {
|
|
565
|
+
const { parent } = current;
|
|
566
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement) {
|
|
567
|
+
if (parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
568
|
+
if (parent.alternate === current && isNegatedInstanceofMapExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
569
|
+
}
|
|
570
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesInstanceofMapOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
571
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
572
|
+
current = parent;
|
|
573
|
+
}
|
|
574
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
575
|
+
}
|
|
576
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
577
|
+
let current = startNode;
|
|
578
|
+
while (current.parent) {
|
|
579
|
+
const { parent } = current;
|
|
580
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
581
|
+
const statements = parent.body;
|
|
582
|
+
let ownIndex = -1;
|
|
583
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
584
|
+
ownIndex = i;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
588
|
+
const sibling = statements[i];
|
|
589
|
+
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedInstanceofMapExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits$1(sibling.consequent)) return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
current = parent;
|
|
593
|
+
}
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
//#endregion
|
|
497
599
|
//#region src/rules/no-mutable-union-array-param.ts
|
|
498
600
|
const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
499
601
|
"push",
|
|
@@ -502,7 +604,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
502
604
|
"fill",
|
|
503
605
|
"copyWithin"
|
|
504
606
|
]);
|
|
505
|
-
const createRule$
|
|
607
|
+
const createRule$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
506
608
|
function isUnionArrayType(typeAnnotation) {
|
|
507
609
|
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
508
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) {
|
|
@@ -510,7 +612,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
510
612
|
if (firstParam?.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
511
613
|
}
|
|
512
614
|
}
|
|
513
|
-
const noMutableUnionArrayParam = createRule$
|
|
615
|
+
const noMutableUnionArrayParam = createRule$5({
|
|
514
616
|
name: "no-mutable-union-array-param",
|
|
515
617
|
meta: {
|
|
516
618
|
type: "problem",
|
|
@@ -636,14 +738,14 @@ const noNonBarrelReexport = {
|
|
|
636
738
|
};
|
|
637
739
|
//#endregion
|
|
638
740
|
//#region src/rules/no-object-assign.ts
|
|
639
|
-
const createRule = _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`);
|
|
640
742
|
function resolveFrom$1(scope, name) {
|
|
641
743
|
for (let current = scope; current; current = current.upper) {
|
|
642
744
|
const found = current.set.get(name);
|
|
643
745
|
if (found) return found;
|
|
644
746
|
}
|
|
645
747
|
}
|
|
646
|
-
const noObjectAssign = createRule({
|
|
748
|
+
const noObjectAssign = createRule$4({
|
|
647
749
|
name: "no-object-assign",
|
|
648
750
|
meta: {
|
|
649
751
|
type: "problem",
|
|
@@ -718,6 +820,378 @@ function resolveFrom(scope, name) {
|
|
|
718
820
|
if (found) return found;
|
|
719
821
|
}
|
|
720
822
|
}
|
|
823
|
+
const noPointlessReassignment = {
|
|
824
|
+
meta: {
|
|
825
|
+
type: "problem",
|
|
826
|
+
fixable: "code",
|
|
827
|
+
schema: [],
|
|
828
|
+
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
829
|
+
},
|
|
830
|
+
create(context) {
|
|
831
|
+
return { VariableDeclarator(node) {
|
|
832
|
+
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
833
|
+
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
834
|
+
const scope = context.sourceCode.getScope(node);
|
|
835
|
+
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
836
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
837
|
+
const aliasName = node.id.name;
|
|
838
|
+
const originalName = node.init.name;
|
|
839
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
840
|
+
context.report({
|
|
841
|
+
node,
|
|
842
|
+
messageId: "pointlessReassignment",
|
|
843
|
+
data: {
|
|
844
|
+
name: aliasName,
|
|
845
|
+
value: originalName
|
|
846
|
+
},
|
|
847
|
+
fix(fixer) {
|
|
848
|
+
const variable = scope.set.get(aliasName);
|
|
849
|
+
if (!variable) return null;
|
|
850
|
+
if (aliasIsAnnotated) return null;
|
|
851
|
+
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
852
|
+
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
853
|
+
if (readRefs.some((reference) => {
|
|
854
|
+
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
855
|
+
if (afterToken?.value === ":") return false;
|
|
856
|
+
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
857
|
+
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
858
|
+
while (token) {
|
|
859
|
+
if (token.value === "{") return true;
|
|
860
|
+
if (token.value === "[" || token.value === "(") return false;
|
|
861
|
+
if (token.value === ":") return false;
|
|
862
|
+
token = context.sourceCode.getTokenBefore(token);
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
})) return null;
|
|
866
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
867
|
+
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
868
|
+
const declaration = node.parent;
|
|
869
|
+
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
870
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
871
|
+
return fixes;
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
} };
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region src/rules/no-set-instanceof-mutation.ts
|
|
879
|
+
const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
880
|
+
"add",
|
|
881
|
+
"delete",
|
|
882
|
+
"clear"
|
|
883
|
+
]);
|
|
884
|
+
const createRule$3 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
885
|
+
function isSetInstanceofExpression(node) {
|
|
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
|
+
}
|
|
888
|
+
function definitelyExits(statement) {
|
|
889
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ThrowStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ContinueStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BreakStatement) return true;
|
|
890
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) {
|
|
891
|
+
const last = statement.body.at(-1);
|
|
892
|
+
return last !== void 0 && definitelyExits(last);
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
const noSetInstanceofMutation = createRule$3({
|
|
897
|
+
name: "no-set-instanceof-mutation",
|
|
898
|
+
meta: {
|
|
899
|
+
type: "problem",
|
|
900
|
+
schema: [],
|
|
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
|
+
},
|
|
904
|
+
defaultOptions: [],
|
|
905
|
+
create(context) {
|
|
906
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
907
|
+
const checker = services.program.getTypeChecker();
|
|
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
|
+
}
|
|
913
|
+
return { CallExpression(node) {
|
|
914
|
+
const { callee } = node;
|
|
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
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
917
|
+
if (!variable) 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
|
+
if (!isGuardedBySetInstanceof(node, variable, context)) return;
|
|
924
|
+
context.report({
|
|
925
|
+
node,
|
|
926
|
+
messageId: "unsound",
|
|
927
|
+
data: { method: callee.property.name }
|
|
928
|
+
});
|
|
929
|
+
} };
|
|
930
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
931
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
932
|
+
}
|
|
933
|
+
function isNegatedSetInstanceofExpression(testNode, target, ruleContext) {
|
|
934
|
+
if (testNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
935
|
+
return matchesSetInstanceofOn(testNode.argument, target, ruleContext);
|
|
936
|
+
}
|
|
937
|
+
function matchesSetInstanceofOn(testNode, target, ruleContext) {
|
|
938
|
+
if (!isSetInstanceofExpression(testNode)) return false;
|
|
939
|
+
const { left } = testNode;
|
|
940
|
+
return left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
|
|
941
|
+
}
|
|
942
|
+
function isGuardedBySetInstanceof(startNode, parameterVariable, ruleContext) {
|
|
943
|
+
let current = startNode;
|
|
944
|
+
while (current.parent) {
|
|
945
|
+
const { parent } = current;
|
|
946
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement) {
|
|
947
|
+
if (parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
948
|
+
if (parent.alternate === current && isNegatedSetInstanceofExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
949
|
+
}
|
|
950
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesSetInstanceofOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
951
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
952
|
+
current = parent;
|
|
953
|
+
}
|
|
954
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
955
|
+
}
|
|
956
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
957
|
+
let current = startNode;
|
|
958
|
+
while (current.parent) {
|
|
959
|
+
const { parent } = current;
|
|
960
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
961
|
+
const statements = parent.body;
|
|
962
|
+
let ownIndex = -1;
|
|
963
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
964
|
+
ownIndex = i;
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
968
|
+
const sibling = statements[i];
|
|
969
|
+
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedSetInstanceofExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
current = parent;
|
|
973
|
+
}
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/rules/no-side-effects-in-index.ts
|
|
980
|
+
const noSideEffectsInIndex = {
|
|
981
|
+
meta: {
|
|
982
|
+
type: "problem",
|
|
983
|
+
schema: [],
|
|
984
|
+
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 }}." }
|
|
985
|
+
},
|
|
986
|
+
create(context) {
|
|
987
|
+
if (!isIndexFile(context.filename)) return {};
|
|
988
|
+
return { Program(node) {
|
|
989
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
990
|
+
node: statement,
|
|
991
|
+
messageId: "notAPureReexport",
|
|
992
|
+
data: { description: statement.type }
|
|
993
|
+
});
|
|
994
|
+
} };
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
999
|
+
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1000
|
+
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
1001
|
+
function isDefinitelyNumberType(type) {
|
|
1002
|
+
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
1003
|
+
return (type.flags & typescript.TypeFlags.NumberLike) !== 0;
|
|
1004
|
+
}
|
|
1005
|
+
const preferNumericSortCompare = createRule$2({
|
|
1006
|
+
name: "prefer-numeric-sort-compare",
|
|
1007
|
+
meta: {
|
|
1008
|
+
type: "suggestion",
|
|
1009
|
+
hasSuggestions: true,
|
|
1010
|
+
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." },
|
|
1011
|
+
schema: [],
|
|
1012
|
+
messages: {
|
|
1013
|
+
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.",
|
|
1014
|
+
addAscendingCompare: "Add an ascending numeric compare function: '(a, b) => a - b'."
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
defaultOptions: [],
|
|
1018
|
+
create(context) {
|
|
1019
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
1020
|
+
const checker = services.program.getTypeChecker();
|
|
1021
|
+
return { CallExpression(node) {
|
|
1022
|
+
if (node.arguments.length > 0) return;
|
|
1023
|
+
const { callee } = node;
|
|
1024
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed) return;
|
|
1025
|
+
if (callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !SORT_METHOD_NAMES.has(callee.property.name)) return;
|
|
1026
|
+
const receiverTsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
|
|
1027
|
+
if (!typescript.isExpression(receiverTsNode)) return;
|
|
1028
|
+
const receiverType = checker.getTypeAtLocation(receiverTsNode);
|
|
1029
|
+
if (!checker.isArrayType(receiverType)) return;
|
|
1030
|
+
if (!(0, ts_api_utils.isTypeReference)(receiverType)) return;
|
|
1031
|
+
const [elementType] = checker.getTypeArguments(receiverType);
|
|
1032
|
+
if (!elementType || !isDefinitelyNumberType(elementType)) return;
|
|
1033
|
+
context.report({
|
|
1034
|
+
node,
|
|
1035
|
+
messageId: "preferNumericCompare",
|
|
1036
|
+
data: { method: callee.property.name },
|
|
1037
|
+
suggest: [{
|
|
1038
|
+
messageId: "addAscendingCompare",
|
|
1039
|
+
fix(fixer) {
|
|
1040
|
+
const closingParen = context.sourceCode.getLastToken(node);
|
|
1041
|
+
if (!closingParen) return null;
|
|
1042
|
+
return fixer.insertTextBefore(closingParen, "(a, b) => a - b");
|
|
1043
|
+
}
|
|
1044
|
+
}]
|
|
1045
|
+
});
|
|
1046
|
+
} };
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/rules/prefer-readonly-array-param.ts
|
|
1051
|
+
const createRule$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1052
|
+
function getFixableArrayOrTupleType(typeNode) {
|
|
1053
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
|
|
1054
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return typeNode;
|
|
1055
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTupleType) return typeNode;
|
|
1056
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference && typeNode.typeName.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && typeNode.typeName.name === "Array") return typeNode;
|
|
1057
|
+
}
|
|
1058
|
+
function getFixableTypesForAnnotation(typeNode) {
|
|
1059
|
+
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeNode.types.flatMap(getFixableTypesForAnnotation);
|
|
1060
|
+
const fixable = getFixableArrayOrTupleType(typeNode);
|
|
1061
|
+
return fixable ? [fixable] : [];
|
|
1062
|
+
}
|
|
1063
|
+
function getAnnotatedParamNode$1(param) {
|
|
1064
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode$1(param.parameter);
|
|
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
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement || param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return param;
|
|
1067
|
+
}
|
|
1068
|
+
const FUNCTION_LIKE_SELECTOR$1 = [
|
|
1069
|
+
"ArrowFunctionExpression",
|
|
1070
|
+
"FunctionDeclaration",
|
|
1071
|
+
"FunctionExpression",
|
|
1072
|
+
"TSCallSignatureDeclaration",
|
|
1073
|
+
"TSConstructSignatureDeclaration",
|
|
1074
|
+
"TSDeclareFunction",
|
|
1075
|
+
"TSEmptyBodyFunctionExpression",
|
|
1076
|
+
"TSFunctionType",
|
|
1077
|
+
"TSMethodSignature"
|
|
1078
|
+
].join(", ");
|
|
1079
|
+
const preferReadonlyArrayParam = createRule$1({
|
|
1080
|
+
name: "prefer-readonly-array-param",
|
|
1081
|
+
meta: {
|
|
1082
|
+
type: "problem",
|
|
1083
|
+
fixable: "code",
|
|
1084
|
+
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." },
|
|
1085
|
+
schema: [],
|
|
1086
|
+
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." }
|
|
1087
|
+
},
|
|
1088
|
+
defaultOptions: [],
|
|
1089
|
+
create(context) {
|
|
1090
|
+
function checkParam(param) {
|
|
1091
|
+
const annotatedNode = getAnnotatedParamNode$1(param);
|
|
1092
|
+
if (!annotatedNode?.typeAnnotation) return;
|
|
1093
|
+
const fixableTypes = getFixableTypesForAnnotation(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1094
|
+
if (fixableTypes.length === 0) return;
|
|
1095
|
+
const suggestion = fixableTypes.map((fixableType) => fixableType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference ? "ReadonlyArray<T>" : `readonly ${fixableType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTupleType ? "[T, U]" : "T[]"}`).join(" / ");
|
|
1096
|
+
context.report({
|
|
1097
|
+
node: param,
|
|
1098
|
+
messageId: "preferReadonly",
|
|
1099
|
+
data: { suggestion },
|
|
1100
|
+
fix(fixer) {
|
|
1101
|
+
return fixableTypes.map((fixableType) => fixableType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference ? fixer.replaceText(fixableType.typeName, "ReadonlyArray") : fixer.insertTextBefore(fixableType, "readonly "));
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
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
|
+
}
|
|
1190
|
+
return { [FUNCTION_LIKE_SELECTOR](node) {
|
|
1191
|
+
for (const param of node.params) checkParam(param);
|
|
1192
|
+
} };
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
721
1195
|
//#endregion
|
|
722
1196
|
//#region src/plugin.ts
|
|
723
1197
|
const plugin = {
|
|
@@ -733,81 +1207,17 @@ const plugin = {
|
|
|
733
1207
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
734
1208
|
"no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
|
|
735
1209
|
"no-index-files": noIndexFiles,
|
|
1210
|
+
"no-map-instanceof-mutation": noMapInstanceofMutation,
|
|
736
1211
|
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
737
1212
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
738
1213
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
739
1214
|
"no-object-assign": noObjectAssign,
|
|
740
|
-
"no-pointless-reassignment":
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
},
|
|
747
|
-
create(context) {
|
|
748
|
-
return { VariableDeclarator(node) {
|
|
749
|
-
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
750
|
-
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
751
|
-
const scope = context.sourceCode.getScope(node);
|
|
752
|
-
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
753
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
754
|
-
const aliasName = node.id.name;
|
|
755
|
-
const originalName = node.init.name;
|
|
756
|
-
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
757
|
-
context.report({
|
|
758
|
-
node,
|
|
759
|
-
messageId: "pointlessReassignment",
|
|
760
|
-
data: {
|
|
761
|
-
name: aliasName,
|
|
762
|
-
value: originalName
|
|
763
|
-
},
|
|
764
|
-
fix(fixer) {
|
|
765
|
-
const variable = scope.set.get(aliasName);
|
|
766
|
-
if (!variable) return null;
|
|
767
|
-
if (aliasIsAnnotated) return null;
|
|
768
|
-
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
769
|
-
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
770
|
-
if (readRefs.some((reference) => {
|
|
771
|
-
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
772
|
-
if (afterToken?.value === ":") return false;
|
|
773
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
774
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
775
|
-
while (token) {
|
|
776
|
-
if (token.value === "{") return true;
|
|
777
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
778
|
-
if (token.value === ":") return false;
|
|
779
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
780
|
-
}
|
|
781
|
-
return false;
|
|
782
|
-
})) return null;
|
|
783
|
-
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
784
|
-
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
785
|
-
const declaration = node.parent;
|
|
786
|
-
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
787
|
-
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
788
|
-
return fixes;
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
} };
|
|
792
|
-
}
|
|
793
|
-
},
|
|
794
|
-
"no-side-effects-in-index": {
|
|
795
|
-
meta: {
|
|
796
|
-
type: "problem",
|
|
797
|
-
schema: [],
|
|
798
|
-
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 }}." }
|
|
799
|
-
},
|
|
800
|
-
create(context) {
|
|
801
|
-
if (!isIndexFile(context.filename)) return {};
|
|
802
|
-
return { Program(node) {
|
|
803
|
-
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
804
|
-
node: statement,
|
|
805
|
-
messageId: "notAPureReexport",
|
|
806
|
-
data: { description: statement.type }
|
|
807
|
-
});
|
|
808
|
-
} };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
1215
|
+
"no-pointless-reassignment": noPointlessReassignment,
|
|
1216
|
+
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1217
|
+
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1218
|
+
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1219
|
+
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1220
|
+
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
811
1221
|
},
|
|
812
1222
|
configs: {
|
|
813
1223
|
get recommended() {
|
|
@@ -818,7 +1228,8 @@ const plugin = {
|
|
|
818
1228
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
819
1229
|
"exadev/no-mutable-union-array-param": "error",
|
|
820
1230
|
"exadev/no-object-assign": "error",
|
|
821
|
-
"exadev/no-pointless-reassignment": "error"
|
|
1231
|
+
"exadev/no-pointless-reassignment": "error",
|
|
1232
|
+
"exadev/prefer-readonly-array-param": "error"
|
|
822
1233
|
}
|
|
823
1234
|
};
|
|
824
1235
|
},
|
|
@@ -844,11 +1255,18 @@ const recommendedTypeChecked = [
|
|
|
844
1255
|
"exadev/no-array-isarray-mutation": "error",
|
|
845
1256
|
"exadev/no-enum-number-widening": "error",
|
|
846
1257
|
"exadev/no-enum-reverse-lookup-widening": "error",
|
|
1258
|
+
"exadev/no-map-instanceof-mutation": "error",
|
|
847
1259
|
"exadev/no-mutable-union-array-param": "error",
|
|
848
1260
|
"exadev/no-object-assign": "error",
|
|
849
1261
|
"exadev/no-pointless-reassignment": "error",
|
|
1262
|
+
"exadev/no-set-instanceof-mutation": "error",
|
|
1263
|
+
"exadev/prefer-numeric-sort-compare": "error",
|
|
1264
|
+
"exadev/prefer-readonly-array-param": "error",
|
|
1265
|
+
"exadev/prefer-readonly-object-param": "error",
|
|
850
1266
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
851
1267
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1268
|
+
"@typescript-eslint/consistent-type-exports": "error",
|
|
1269
|
+
"@typescript-eslint/consistent-type-imports": "error",
|
|
852
1270
|
"@typescript-eslint/method-signature-style": ["error", "property"],
|
|
853
1271
|
"@typescript-eslint/no-deprecated": "error",
|
|
854
1272
|
"@typescript-eslint/no-magic-numbers": ["error", {
|
|
@@ -868,6 +1286,7 @@ const recommendedTypeChecked = [
|
|
|
868
1286
|
"@typescript-eslint/no-non-null-assertion": "error",
|
|
869
1287
|
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
870
1288
|
"@typescript-eslint/prefer-readonly": "error",
|
|
1289
|
+
"@typescript-eslint/promise-function-async": "error",
|
|
871
1290
|
"@typescript-eslint/require-array-sort-compare": "error",
|
|
872
1291
|
"@typescript-eslint/strict-boolean-expressions": "error",
|
|
873
1292
|
"@typescript-eslint/switch-exhaustiveness-check": "error",
|