@exadev/eslint-config 2.3.0 → 2.5.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 -1
- package/dist/index.cjs +600 -79
- package/dist/index.js +600 -79
- 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.5.0";
|
|
35
36
|
//#endregion
|
|
36
37
|
//#region src/rules/barrel-helpers.ts
|
|
37
38
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -267,6 +268,109 @@ const barrelPolicy = {
|
|
|
267
268
|
}
|
|
268
269
|
};
|
|
269
270
|
//#endregion
|
|
271
|
+
//#region src/rules/no-array-isarray-mutation.ts
|
|
272
|
+
const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
273
|
+
"push",
|
|
274
|
+
"unshift",
|
|
275
|
+
"splice",
|
|
276
|
+
"fill",
|
|
277
|
+
"copyWithin"
|
|
278
|
+
]);
|
|
279
|
+
const createRule$6 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
280
|
+
function isArrayIsArrayCall(node) {
|
|
281
|
+
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && node.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
282
|
+
}
|
|
283
|
+
function definitelyExits$2(statement) {
|
|
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;
|
|
285
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) {
|
|
286
|
+
const last = statement.body.at(-1);
|
|
287
|
+
return last !== void 0 && definitelyExits$2(last);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const noArrayIsArrayMutation = createRule$6({
|
|
292
|
+
name: "no-array-isarray-mutation",
|
|
293
|
+
meta: {
|
|
294
|
+
type: "problem",
|
|
295
|
+
schema: [],
|
|
296
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
297
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a caller's genuinely readonly array can be mutated here even though the parameter's real type includes a readonly array. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
298
|
+
},
|
|
299
|
+
defaultOptions: [],
|
|
300
|
+
create(context) {
|
|
301
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
302
|
+
const checker = services.program.getTypeChecker();
|
|
303
|
+
function parameterHasReadonlyArrayConstituent(parameterNode) {
|
|
304
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
305
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
306
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
307
|
+
}
|
|
308
|
+
return { CallExpression(node) {
|
|
309
|
+
const { callee } = node;
|
|
310
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
311
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
312
|
+
if (!variable) return;
|
|
313
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
|
|
314
|
+
if (!parameterDefinition) return;
|
|
315
|
+
const parameterNode = parameterDefinition.name;
|
|
316
|
+
if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
317
|
+
if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
|
|
318
|
+
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
319
|
+
context.report({
|
|
320
|
+
node,
|
|
321
|
+
messageId: "unsound",
|
|
322
|
+
data: { method: callee.property.name }
|
|
323
|
+
});
|
|
324
|
+
} };
|
|
325
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
326
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
327
|
+
}
|
|
328
|
+
function isNegatedArrayIsArrayCall(testNode, target, ruleContext) {
|
|
329
|
+
if (testNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
330
|
+
return matchesArrayIsArrayOn(testNode.argument, target, ruleContext);
|
|
331
|
+
}
|
|
332
|
+
function matchesArrayIsArrayOn(testNode, target, ruleContext) {
|
|
333
|
+
if (!isArrayIsArrayCall(testNode)) return false;
|
|
334
|
+
const [argument] = testNode.arguments;
|
|
335
|
+
return argument?.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && resolvesToVariable(argument, target, testNode, ruleContext);
|
|
336
|
+
}
|
|
337
|
+
function isGuardedByArrayIsArray(startNode, parameterVariable, ruleContext) {
|
|
338
|
+
let current = startNode;
|
|
339
|
+
while (current.parent) {
|
|
340
|
+
const { parent } = current;
|
|
341
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement) {
|
|
342
|
+
if (parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
343
|
+
if (parent.alternate === current && isNegatedArrayIsArrayCall(parent.test, parameterVariable, ruleContext)) return true;
|
|
344
|
+
}
|
|
345
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesArrayIsArrayOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
346
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
347
|
+
current = parent;
|
|
348
|
+
}
|
|
349
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
350
|
+
}
|
|
351
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
352
|
+
let current = startNode;
|
|
353
|
+
while (current.parent) {
|
|
354
|
+
const { parent } = current;
|
|
355
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
356
|
+
const statements = parent.body;
|
|
357
|
+
let ownIndex = -1;
|
|
358
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
359
|
+
ownIndex = i;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
363
|
+
const sibling = statements[i];
|
|
364
|
+
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent)) return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
current = parent;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
//#endregion
|
|
270
374
|
//#region src/rules/no-enum-number-widening.ts
|
|
271
375
|
const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
272
376
|
name: "no-enum-number-widening",
|
|
@@ -312,6 +416,67 @@ const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((n
|
|
|
312
416
|
}
|
|
313
417
|
});
|
|
314
418
|
//#endregion
|
|
419
|
+
//#region src/rules/no-enum-reverse-lookup-widening.ts
|
|
420
|
+
const noEnumReverseLookupWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
421
|
+
name: "no-enum-reverse-lookup-widening",
|
|
422
|
+
meta: {
|
|
423
|
+
type: "problem",
|
|
424
|
+
hasSuggestions: true,
|
|
425
|
+
docs: { description: "Disallow indexing a numeric enum's reverse mapping with a bare (non-literal) number -- TypeScript types the result as plain 'string' for any number, including one outside the enum's actual member range, where it genuinely returns 'undefined' at runtime." },
|
|
426
|
+
schema: [],
|
|
427
|
+
messages: {
|
|
428
|
+
widening: "Indexing the numeric enum '{{ enumName }}' with a plain 'number' relies on its reverse mapping, which TypeScript types as 'string' for any number -- including one outside the enum's actual members, where this genuinely returns 'undefined' at runtime. Narrow the index to a known member first (a runtime membership check against the enum's own values), or accept that the result may be 'undefined' and handle it.",
|
|
429
|
+
suggestWidenAnnotation: "Widen this variable's annotation to 'string | undefined' so later uses of it as a bare 'string' surface as real compile errors you can resolve."
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
defaultOptions: [],
|
|
433
|
+
create(context) {
|
|
434
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
435
|
+
const checker = services.program.getTypeChecker();
|
|
436
|
+
return { MemberExpression(node) {
|
|
437
|
+
if (!node.computed) return;
|
|
438
|
+
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
|
|
439
|
+
if (!typescript.isExpression(objectTsNode)) return;
|
|
440
|
+
const objectType = checker.getTypeAtLocation(objectTsNode);
|
|
441
|
+
const objectSymbol = objectType.getSymbol();
|
|
442
|
+
if (!objectSymbol || !(objectSymbol.flags & typescript.SymbolFlags.Enum)) return;
|
|
443
|
+
if (!checker.getIndexInfoOfType(objectType, typescript.IndexKind.Number)) return;
|
|
444
|
+
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(node.property);
|
|
445
|
+
if (!typescript.isExpression(propertyTsNode)) return;
|
|
446
|
+
const rawPropertyType = checker.getTypeAtLocation(propertyTsNode);
|
|
447
|
+
const propertyType = checker.getBaseConstraintOfType(rawPropertyType) ?? rawPropertyType;
|
|
448
|
+
if (propertyType.flags & typescript.TypeFlags.EnumLike) {
|
|
449
|
+
if (checker.isTypeAssignableTo(propertyType, checker.getDeclaredTypeOfSymbol(objectSymbol))) return;
|
|
450
|
+
} else {
|
|
451
|
+
if (propertyType.isLiteral()) return;
|
|
452
|
+
if (!(propertyType.flags & typescript.TypeFlags.NumberLike)) return;
|
|
453
|
+
}
|
|
454
|
+
const enumName = checker.typeToString(checker.getDeclaredTypeOfSymbol(objectSymbol));
|
|
455
|
+
const parent = node.parent;
|
|
456
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator && parent.init === node && parent.id.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && parent.id.typeAnnotation?.typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSStringKeyword) {
|
|
457
|
+
const stringKeyword = parent.id.typeAnnotation.typeAnnotation;
|
|
458
|
+
context.report({
|
|
459
|
+
node,
|
|
460
|
+
messageId: "widening",
|
|
461
|
+
data: { enumName },
|
|
462
|
+
suggest: [{
|
|
463
|
+
messageId: "suggestWidenAnnotation",
|
|
464
|
+
fix(fixer) {
|
|
465
|
+
return fixer.replaceText(stringKeyword, "string | undefined");
|
|
466
|
+
}
|
|
467
|
+
}]
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
context.report({
|
|
472
|
+
node,
|
|
473
|
+
messageId: "widening",
|
|
474
|
+
data: { enumName }
|
|
475
|
+
});
|
|
476
|
+
} };
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
//#endregion
|
|
315
480
|
//#region src/rules/no-index-files.ts
|
|
316
481
|
const noIndexFiles = {
|
|
317
482
|
meta: {
|
|
@@ -330,6 +495,107 @@ const noIndexFiles = {
|
|
|
330
495
|
}
|
|
331
496
|
};
|
|
332
497
|
//#endregion
|
|
498
|
+
//#region src/rules/no-map-instanceof-mutation.ts
|
|
499
|
+
const createRule$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
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$5({
|
|
517
|
+
name: "no-map-instanceof-mutation",
|
|
518
|
+
meta: {
|
|
519
|
+
type: "problem",
|
|
520
|
+
schema: [],
|
|
521
|
+
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
522
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a caller's genuinely read-only ReadonlyMap can be mutated here even though the parameter's real type includes ReadonlyMap. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
|
|
523
|
+
},
|
|
524
|
+
defaultOptions: [],
|
|
525
|
+
create(context) {
|
|
526
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
527
|
+
const checker = services.program.getTypeChecker();
|
|
528
|
+
function parameterHasReadonlyMapConstituent(parameterNode) {
|
|
529
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
530
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
531
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
|
|
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 parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
|
|
539
|
+
if (!parameterDefinition) return;
|
|
540
|
+
const parameterNode = parameterDefinition.name;
|
|
541
|
+
if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
542
|
+
if (!parameterHasReadonlyMapConstituent(parameterNode)) return;
|
|
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
|
|
333
599
|
//#region src/rules/no-mutable-union-array-param.ts
|
|
334
600
|
const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
335
601
|
"push",
|
|
@@ -338,7 +604,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
338
604
|
"fill",
|
|
339
605
|
"copyWithin"
|
|
340
606
|
]);
|
|
341
|
-
const createRule$
|
|
607
|
+
const createRule$4 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
342
608
|
function isUnionArrayType(typeAnnotation) {
|
|
343
609
|
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
344
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) {
|
|
@@ -346,7 +612,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
346
612
|
if (firstParam?.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
347
613
|
}
|
|
348
614
|
}
|
|
349
|
-
const noMutableUnionArrayParam = createRule$
|
|
615
|
+
const noMutableUnionArrayParam = createRule$4({
|
|
350
616
|
name: "no-mutable-union-array-param",
|
|
351
617
|
meta: {
|
|
352
618
|
type: "problem",
|
|
@@ -472,14 +738,14 @@ const noNonBarrelReexport = {
|
|
|
472
738
|
};
|
|
473
739
|
//#endregion
|
|
474
740
|
//#region src/rules/no-object-assign.ts
|
|
475
|
-
const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
741
|
+
const createRule$3 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
476
742
|
function resolveFrom$1(scope, name) {
|
|
477
743
|
for (let current = scope; current; current = current.upper) {
|
|
478
744
|
const found = current.set.get(name);
|
|
479
745
|
if (found) return found;
|
|
480
746
|
}
|
|
481
747
|
}
|
|
482
|
-
const noObjectAssign = createRule({
|
|
748
|
+
const noObjectAssign = createRule$3({
|
|
483
749
|
name: "no-object-assign",
|
|
484
750
|
meta: {
|
|
485
751
|
type: "problem",
|
|
@@ -554,6 +820,293 @@ function resolveFrom(scope, name) {
|
|
|
554
820
|
if (found) return found;
|
|
555
821
|
}
|
|
556
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$2 = _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$2({
|
|
897
|
+
name: "no-set-instanceof-mutation",
|
|
898
|
+
meta: {
|
|
899
|
+
type: "problem",
|
|
900
|
+
schema: [],
|
|
901
|
+
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
902
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a caller's genuinely read-only set can be mutated here even though the parameter's real type includes a ReadonlySet. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
|
|
903
|
+
},
|
|
904
|
+
defaultOptions: [],
|
|
905
|
+
create(context) {
|
|
906
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
907
|
+
const checker = services.program.getTypeChecker();
|
|
908
|
+
function parameterHasReadonlySetConstituent(parameterNode) {
|
|
909
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
910
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
911
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
|
|
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 parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
|
|
919
|
+
if (!parameterDefinition) return;
|
|
920
|
+
const parameterNode = parameterDefinition.name;
|
|
921
|
+
if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
922
|
+
if (!parameterHasReadonlySetConstituent(parameterNode)) return;
|
|
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$1 = _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$1({
|
|
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 = _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(param) {
|
|
1064
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(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 = [
|
|
1069
|
+
"ArrowFunctionExpression",
|
|
1070
|
+
"FunctionDeclaration",
|
|
1071
|
+
"FunctionExpression",
|
|
1072
|
+
"TSCallSignatureDeclaration",
|
|
1073
|
+
"TSConstructSignatureDeclaration",
|
|
1074
|
+
"TSDeclareFunction",
|
|
1075
|
+
"TSEmptyBodyFunctionExpression",
|
|
1076
|
+
"TSFunctionType",
|
|
1077
|
+
"TSMethodSignature"
|
|
1078
|
+
].join(", ");
|
|
1079
|
+
const preferReadonlyArrayParam = createRule({
|
|
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(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](node) {
|
|
1106
|
+
for (const param of node.params) checkParam(param);
|
|
1107
|
+
} };
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
557
1110
|
//#endregion
|
|
558
1111
|
//#region src/plugin.ts
|
|
559
1112
|
const plugin = {
|
|
@@ -565,83 +1118,20 @@ const plugin = {
|
|
|
565
1118
|
rules: {
|
|
566
1119
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
567
1120
|
"barrel-policy": barrelPolicy,
|
|
1121
|
+
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
568
1122
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
1123
|
+
"no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
|
|
569
1124
|
"no-index-files": noIndexFiles,
|
|
1125
|
+
"no-map-instanceof-mutation": noMapInstanceofMutation,
|
|
570
1126
|
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
571
1127
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
572
1128
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
573
1129
|
"no-object-assign": noObjectAssign,
|
|
574
|
-
"no-pointless-reassignment":
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
580
|
-
},
|
|
581
|
-
create(context) {
|
|
582
|
-
return { VariableDeclarator(node) {
|
|
583
|
-
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
584
|
-
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
585
|
-
const scope = context.sourceCode.getScope(node);
|
|
586
|
-
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
587
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
588
|
-
const aliasName = node.id.name;
|
|
589
|
-
const originalName = node.init.name;
|
|
590
|
-
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
591
|
-
context.report({
|
|
592
|
-
node,
|
|
593
|
-
messageId: "pointlessReassignment",
|
|
594
|
-
data: {
|
|
595
|
-
name: aliasName,
|
|
596
|
-
value: originalName
|
|
597
|
-
},
|
|
598
|
-
fix(fixer) {
|
|
599
|
-
const variable = scope.set.get(aliasName);
|
|
600
|
-
if (!variable) return null;
|
|
601
|
-
if (aliasIsAnnotated) return null;
|
|
602
|
-
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
603
|
-
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
604
|
-
if (readRefs.some((reference) => {
|
|
605
|
-
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
606
|
-
if (afterToken?.value === ":") return false;
|
|
607
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
608
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
609
|
-
while (token) {
|
|
610
|
-
if (token.value === "{") return true;
|
|
611
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
612
|
-
if (token.value === ":") return false;
|
|
613
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
614
|
-
}
|
|
615
|
-
return false;
|
|
616
|
-
})) return null;
|
|
617
|
-
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
618
|
-
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
619
|
-
const declaration = node.parent;
|
|
620
|
-
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
621
|
-
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
622
|
-
return fixes;
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
} };
|
|
626
|
-
}
|
|
627
|
-
},
|
|
628
|
-
"no-side-effects-in-index": {
|
|
629
|
-
meta: {
|
|
630
|
-
type: "problem",
|
|
631
|
-
schema: [],
|
|
632
|
-
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 }}." }
|
|
633
|
-
},
|
|
634
|
-
create(context) {
|
|
635
|
-
if (!isIndexFile(context.filename)) return {};
|
|
636
|
-
return { Program(node) {
|
|
637
|
-
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
638
|
-
node: statement,
|
|
639
|
-
messageId: "notAPureReexport",
|
|
640
|
-
data: { description: statement.type }
|
|
641
|
-
});
|
|
642
|
-
} };
|
|
643
|
-
}
|
|
644
|
-
}
|
|
1130
|
+
"no-pointless-reassignment": noPointlessReassignment,
|
|
1131
|
+
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1132
|
+
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1133
|
+
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1134
|
+
"prefer-readonly-array-param": preferReadonlyArrayParam
|
|
645
1135
|
},
|
|
646
1136
|
configs: {
|
|
647
1137
|
get recommended() {
|
|
@@ -652,7 +1142,8 @@ const plugin = {
|
|
|
652
1142
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
653
1143
|
"exadev/no-mutable-union-array-param": "error",
|
|
654
1144
|
"exadev/no-object-assign": "error",
|
|
655
|
-
"exadev/no-pointless-reassignment": "error"
|
|
1145
|
+
"exadev/no-pointless-reassignment": "error",
|
|
1146
|
+
"exadev/prefer-readonly-array-param": "error"
|
|
656
1147
|
}
|
|
657
1148
|
};
|
|
658
1149
|
},
|
|
@@ -675,14 +1166,44 @@ const recommendedTypeChecked = [
|
|
|
675
1166
|
linterOptions: { noInlineConfig: true },
|
|
676
1167
|
rules: {
|
|
677
1168
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
1169
|
+
"exadev/no-array-isarray-mutation": "error",
|
|
678
1170
|
"exadev/no-enum-number-widening": "error",
|
|
1171
|
+
"exadev/no-enum-reverse-lookup-widening": "error",
|
|
1172
|
+
"exadev/no-map-instanceof-mutation": "error",
|
|
679
1173
|
"exadev/no-mutable-union-array-param": "error",
|
|
680
1174
|
"exadev/no-object-assign": "error",
|
|
681
1175
|
"exadev/no-pointless-reassignment": "error",
|
|
682
|
-
"
|
|
1176
|
+
"exadev/no-set-instanceof-mutation": "error",
|
|
1177
|
+
"exadev/prefer-numeric-sort-compare": "error",
|
|
1178
|
+
"exadev/prefer-readonly-array-param": "error",
|
|
683
1179
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
1180
|
+
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1181
|
+
"@typescript-eslint/consistent-type-exports": "error",
|
|
1182
|
+
"@typescript-eslint/consistent-type-imports": "error",
|
|
684
1183
|
"@typescript-eslint/method-signature-style": ["error", "property"],
|
|
685
|
-
"@typescript-eslint/no-
|
|
1184
|
+
"@typescript-eslint/no-deprecated": "error",
|
|
1185
|
+
"@typescript-eslint/no-magic-numbers": ["error", {
|
|
1186
|
+
ignore: [
|
|
1187
|
+
-1,
|
|
1188
|
+
0,
|
|
1189
|
+
1,
|
|
1190
|
+
2
|
|
1191
|
+
],
|
|
1192
|
+
ignoreArrayIndexes: true,
|
|
1193
|
+
ignoreEnums: true,
|
|
1194
|
+
ignoreReadonlyClassProperties: true,
|
|
1195
|
+
ignoreDefaultValues: true
|
|
1196
|
+
}],
|
|
1197
|
+
"@typescript-eslint/no-misused-spread": "error",
|
|
1198
|
+
"@typescript-eslint/no-mixed-enums": "error",
|
|
1199
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
1200
|
+
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
1201
|
+
"@typescript-eslint/prefer-readonly": "error",
|
|
1202
|
+
"@typescript-eslint/promise-function-async": "error",
|
|
1203
|
+
"@typescript-eslint/require-array-sort-compare": "error",
|
|
1204
|
+
"@typescript-eslint/strict-boolean-expressions": "error",
|
|
1205
|
+
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
1206
|
+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error"
|
|
686
1207
|
}
|
|
687
1208
|
},
|
|
688
1209
|
{
|