@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.js
CHANGED
|
@@ -2,8 +2,9 @@ import tseslint from "typescript-eslint";
|
|
|
2
2
|
import { posix } from "node:path";
|
|
3
3
|
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
4
4
|
import * as ts from "typescript";
|
|
5
|
+
import { isTypeReference } from "ts-api-utils";
|
|
5
6
|
//#region package.json
|
|
6
|
-
var version = "2.
|
|
7
|
+
var version = "2.5.0";
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region src/rules/barrel-helpers.ts
|
|
9
10
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -239,6 +240,109 @@ const barrelPolicy = {
|
|
|
239
240
|
}
|
|
240
241
|
};
|
|
241
242
|
//#endregion
|
|
243
|
+
//#region src/rules/no-array-isarray-mutation.ts
|
|
244
|
+
const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
245
|
+
"push",
|
|
246
|
+
"unshift",
|
|
247
|
+
"splice",
|
|
248
|
+
"fill",
|
|
249
|
+
"copyWithin"
|
|
250
|
+
]);
|
|
251
|
+
const createRule$6 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
252
|
+
function isArrayIsArrayCall(node) {
|
|
253
|
+
return node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
254
|
+
}
|
|
255
|
+
function definitelyExits$2(statement) {
|
|
256
|
+
if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
|
|
257
|
+
if (statement.type === AST_NODE_TYPES.BlockStatement) {
|
|
258
|
+
const last = statement.body.at(-1);
|
|
259
|
+
return last !== void 0 && definitelyExits$2(last);
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const noArrayIsArrayMutation = createRule$6({
|
|
264
|
+
name: "no-array-isarray-mutation",
|
|
265
|
+
meta: {
|
|
266
|
+
type: "problem",
|
|
267
|
+
schema: [],
|
|
268
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
269
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a caller's genuinely readonly array can be mutated here even though the parameter's real type includes a readonly array. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
270
|
+
},
|
|
271
|
+
defaultOptions: [],
|
|
272
|
+
create(context) {
|
|
273
|
+
const services = ESLintUtils.getParserServices(context);
|
|
274
|
+
const checker = services.program.getTypeChecker();
|
|
275
|
+
function parameterHasReadonlyArrayConstituent(parameterNode) {
|
|
276
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
277
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
278
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
279
|
+
}
|
|
280
|
+
return { CallExpression(node) {
|
|
281
|
+
const { callee } = node;
|
|
282
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
283
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
284
|
+
if (!variable) return;
|
|
285
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
|
|
286
|
+
if (!parameterDefinition) return;
|
|
287
|
+
const parameterNode = parameterDefinition.name;
|
|
288
|
+
if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
289
|
+
if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
|
|
290
|
+
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
291
|
+
context.report({
|
|
292
|
+
node,
|
|
293
|
+
messageId: "unsound",
|
|
294
|
+
data: { method: callee.property.name }
|
|
295
|
+
});
|
|
296
|
+
} };
|
|
297
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
298
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
299
|
+
}
|
|
300
|
+
function isNegatedArrayIsArrayCall(testNode, target, ruleContext) {
|
|
301
|
+
if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
302
|
+
return matchesArrayIsArrayOn(testNode.argument, target, ruleContext);
|
|
303
|
+
}
|
|
304
|
+
function matchesArrayIsArrayOn(testNode, target, ruleContext) {
|
|
305
|
+
if (!isArrayIsArrayCall(testNode)) return false;
|
|
306
|
+
const [argument] = testNode.arguments;
|
|
307
|
+
return argument?.type === AST_NODE_TYPES.Identifier && resolvesToVariable(argument, target, testNode, ruleContext);
|
|
308
|
+
}
|
|
309
|
+
function isGuardedByArrayIsArray(startNode, parameterVariable, ruleContext) {
|
|
310
|
+
let current = startNode;
|
|
311
|
+
while (current.parent) {
|
|
312
|
+
const { parent } = current;
|
|
313
|
+
if (parent.type === AST_NODE_TYPES.IfStatement) {
|
|
314
|
+
if (parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
315
|
+
if (parent.alternate === current && isNegatedArrayIsArrayCall(parent.test, parameterVariable, ruleContext)) return true;
|
|
316
|
+
}
|
|
317
|
+
if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesArrayIsArrayOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
318
|
+
if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
319
|
+
current = parent;
|
|
320
|
+
}
|
|
321
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
322
|
+
}
|
|
323
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
324
|
+
let current = startNode;
|
|
325
|
+
while (current.parent) {
|
|
326
|
+
const { parent } = current;
|
|
327
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
|
|
328
|
+
const statements = parent.body;
|
|
329
|
+
let ownIndex = -1;
|
|
330
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
331
|
+
ownIndex = i;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
335
|
+
const sibling = statements[i];
|
|
336
|
+
if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent)) return true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
current = parent;
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
//#endregion
|
|
242
346
|
//#region src/rules/no-enum-number-widening.ts
|
|
243
347
|
const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
244
348
|
name: "no-enum-number-widening",
|
|
@@ -284,6 +388,67 @@ const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.c
|
|
|
284
388
|
}
|
|
285
389
|
});
|
|
286
390
|
//#endregion
|
|
391
|
+
//#region src/rules/no-enum-reverse-lookup-widening.ts
|
|
392
|
+
const noEnumReverseLookupWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
393
|
+
name: "no-enum-reverse-lookup-widening",
|
|
394
|
+
meta: {
|
|
395
|
+
type: "problem",
|
|
396
|
+
hasSuggestions: true,
|
|
397
|
+
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." },
|
|
398
|
+
schema: [],
|
|
399
|
+
messages: {
|
|
400
|
+
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.",
|
|
401
|
+
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."
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
defaultOptions: [],
|
|
405
|
+
create(context) {
|
|
406
|
+
const services = ESLintUtils.getParserServices(context);
|
|
407
|
+
const checker = services.program.getTypeChecker();
|
|
408
|
+
return { MemberExpression(node) {
|
|
409
|
+
if (!node.computed) return;
|
|
410
|
+
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
|
|
411
|
+
if (!ts.isExpression(objectTsNode)) return;
|
|
412
|
+
const objectType = checker.getTypeAtLocation(objectTsNode);
|
|
413
|
+
const objectSymbol = objectType.getSymbol();
|
|
414
|
+
if (!objectSymbol || !(objectSymbol.flags & ts.SymbolFlags.Enum)) return;
|
|
415
|
+
if (!checker.getIndexInfoOfType(objectType, ts.IndexKind.Number)) return;
|
|
416
|
+
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(node.property);
|
|
417
|
+
if (!ts.isExpression(propertyTsNode)) return;
|
|
418
|
+
const rawPropertyType = checker.getTypeAtLocation(propertyTsNode);
|
|
419
|
+
const propertyType = checker.getBaseConstraintOfType(rawPropertyType) ?? rawPropertyType;
|
|
420
|
+
if (propertyType.flags & ts.TypeFlags.EnumLike) {
|
|
421
|
+
if (checker.isTypeAssignableTo(propertyType, checker.getDeclaredTypeOfSymbol(objectSymbol))) return;
|
|
422
|
+
} else {
|
|
423
|
+
if (propertyType.isLiteral()) return;
|
|
424
|
+
if (!(propertyType.flags & ts.TypeFlags.NumberLike)) return;
|
|
425
|
+
}
|
|
426
|
+
const enumName = checker.typeToString(checker.getDeclaredTypeOfSymbol(objectSymbol));
|
|
427
|
+
const parent = node.parent;
|
|
428
|
+
if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init === node && parent.id.type === AST_NODE_TYPES.Identifier && parent.id.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSStringKeyword) {
|
|
429
|
+
const stringKeyword = parent.id.typeAnnotation.typeAnnotation;
|
|
430
|
+
context.report({
|
|
431
|
+
node,
|
|
432
|
+
messageId: "widening",
|
|
433
|
+
data: { enumName },
|
|
434
|
+
suggest: [{
|
|
435
|
+
messageId: "suggestWidenAnnotation",
|
|
436
|
+
fix(fixer) {
|
|
437
|
+
return fixer.replaceText(stringKeyword, "string | undefined");
|
|
438
|
+
}
|
|
439
|
+
}]
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
context.report({
|
|
444
|
+
node,
|
|
445
|
+
messageId: "widening",
|
|
446
|
+
data: { enumName }
|
|
447
|
+
});
|
|
448
|
+
} };
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
//#endregion
|
|
287
452
|
//#region src/rules/no-index-files.ts
|
|
288
453
|
const noIndexFiles = {
|
|
289
454
|
meta: {
|
|
@@ -302,6 +467,107 @@ const noIndexFiles = {
|
|
|
302
467
|
}
|
|
303
468
|
};
|
|
304
469
|
//#endregion
|
|
470
|
+
//#region src/rules/no-map-instanceof-mutation.ts
|
|
471
|
+
const createRule$5 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
472
|
+
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
473
|
+
"set",
|
|
474
|
+
"delete",
|
|
475
|
+
"clear"
|
|
476
|
+
]);
|
|
477
|
+
function isInstanceofMapExpression(node) {
|
|
478
|
+
return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Map";
|
|
479
|
+
}
|
|
480
|
+
function definitelyExits$1(statement) {
|
|
481
|
+
if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
|
|
482
|
+
if (statement.type === AST_NODE_TYPES.BlockStatement) {
|
|
483
|
+
const last = statement.body.at(-1);
|
|
484
|
+
return last !== void 0 && definitelyExits$1(last);
|
|
485
|
+
}
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const noMapInstanceofMutation = createRule$5({
|
|
489
|
+
name: "no-map-instanceof-mutation",
|
|
490
|
+
meta: {
|
|
491
|
+
type: "problem",
|
|
492
|
+
schema: [],
|
|
493
|
+
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
494
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by 'instanceof Map' -- Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a caller's genuinely read-only ReadonlyMap can be mutated here even though the parameter's real type includes ReadonlyMap. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
|
|
495
|
+
},
|
|
496
|
+
defaultOptions: [],
|
|
497
|
+
create(context) {
|
|
498
|
+
const services = ESLintUtils.getParserServices(context);
|
|
499
|
+
const checker = services.program.getTypeChecker();
|
|
500
|
+
function parameterHasReadonlyMapConstituent(parameterNode) {
|
|
501
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
502
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
503
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlyMap");
|
|
504
|
+
}
|
|
505
|
+
return { CallExpression(node) {
|
|
506
|
+
const { callee } = node;
|
|
507
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_MAP_METHODS.has(callee.property.name)) return;
|
|
508
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
509
|
+
if (!variable) return;
|
|
510
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
|
|
511
|
+
if (!parameterDefinition) return;
|
|
512
|
+
const parameterNode = parameterDefinition.name;
|
|
513
|
+
if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
514
|
+
if (!parameterHasReadonlyMapConstituent(parameterNode)) return;
|
|
515
|
+
if (!isGuardedByInstanceofMap(node, variable, context)) return;
|
|
516
|
+
context.report({
|
|
517
|
+
node,
|
|
518
|
+
messageId: "unsound",
|
|
519
|
+
data: { method: callee.property.name }
|
|
520
|
+
});
|
|
521
|
+
} };
|
|
522
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
523
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
524
|
+
}
|
|
525
|
+
function isNegatedInstanceofMapExpression(testNode, target, ruleContext) {
|
|
526
|
+
if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
527
|
+
return matchesInstanceofMapOn(testNode.argument, target, ruleContext);
|
|
528
|
+
}
|
|
529
|
+
function matchesInstanceofMapOn(testNode, target, ruleContext) {
|
|
530
|
+
if (!isInstanceofMapExpression(testNode)) return false;
|
|
531
|
+
const { left } = testNode;
|
|
532
|
+
return left.type === AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
|
|
533
|
+
}
|
|
534
|
+
function isGuardedByInstanceofMap(startNode, parameterVariable, ruleContext) {
|
|
535
|
+
let current = startNode;
|
|
536
|
+
while (current.parent) {
|
|
537
|
+
const { parent } = current;
|
|
538
|
+
if (parent.type === AST_NODE_TYPES.IfStatement) {
|
|
539
|
+
if (parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
540
|
+
if (parent.alternate === current && isNegatedInstanceofMapExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
541
|
+
}
|
|
542
|
+
if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesInstanceofMapOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
543
|
+
if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
544
|
+
current = parent;
|
|
545
|
+
}
|
|
546
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
547
|
+
}
|
|
548
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
549
|
+
let current = startNode;
|
|
550
|
+
while (current.parent) {
|
|
551
|
+
const { parent } = current;
|
|
552
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
|
|
553
|
+
const statements = parent.body;
|
|
554
|
+
let ownIndex = -1;
|
|
555
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
556
|
+
ownIndex = i;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
560
|
+
const sibling = statements[i];
|
|
561
|
+
if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedInstanceofMapExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits$1(sibling.consequent)) return true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
current = parent;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
//#endregion
|
|
305
571
|
//#region src/rules/no-mutable-union-array-param.ts
|
|
306
572
|
const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
307
573
|
"push",
|
|
@@ -310,7 +576,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
310
576
|
"fill",
|
|
311
577
|
"copyWithin"
|
|
312
578
|
]);
|
|
313
|
-
const createRule$
|
|
579
|
+
const createRule$4 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
314
580
|
function isUnionArrayType(typeAnnotation) {
|
|
315
581
|
if (typeAnnotation.type === AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
316
582
|
if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "Array" && typeAnnotation.typeArguments?.params.length === 1) {
|
|
@@ -318,7 +584,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
318
584
|
if (firstParam?.type === AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
319
585
|
}
|
|
320
586
|
}
|
|
321
|
-
const noMutableUnionArrayParam = createRule$
|
|
587
|
+
const noMutableUnionArrayParam = createRule$4({
|
|
322
588
|
name: "no-mutable-union-array-param",
|
|
323
589
|
meta: {
|
|
324
590
|
type: "problem",
|
|
@@ -444,14 +710,14 @@ const noNonBarrelReexport = {
|
|
|
444
710
|
};
|
|
445
711
|
//#endregion
|
|
446
712
|
//#region src/rules/no-object-assign.ts
|
|
447
|
-
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
713
|
+
const createRule$3 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
448
714
|
function resolveFrom$1(scope, name) {
|
|
449
715
|
for (let current = scope; current; current = current.upper) {
|
|
450
716
|
const found = current.set.get(name);
|
|
451
717
|
if (found) return found;
|
|
452
718
|
}
|
|
453
719
|
}
|
|
454
|
-
const noObjectAssign = createRule({
|
|
720
|
+
const noObjectAssign = createRule$3({
|
|
455
721
|
name: "no-object-assign",
|
|
456
722
|
meta: {
|
|
457
723
|
type: "problem",
|
|
@@ -526,6 +792,293 @@ function resolveFrom(scope, name) {
|
|
|
526
792
|
if (found) return found;
|
|
527
793
|
}
|
|
528
794
|
}
|
|
795
|
+
const noPointlessReassignment = {
|
|
796
|
+
meta: {
|
|
797
|
+
type: "problem",
|
|
798
|
+
fixable: "code",
|
|
799
|
+
schema: [],
|
|
800
|
+
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
801
|
+
},
|
|
802
|
+
create(context) {
|
|
803
|
+
return { VariableDeclarator(node) {
|
|
804
|
+
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
805
|
+
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
806
|
+
const scope = context.sourceCode.getScope(node);
|
|
807
|
+
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
808
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
809
|
+
const aliasName = node.id.name;
|
|
810
|
+
const originalName = node.init.name;
|
|
811
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
812
|
+
context.report({
|
|
813
|
+
node,
|
|
814
|
+
messageId: "pointlessReassignment",
|
|
815
|
+
data: {
|
|
816
|
+
name: aliasName,
|
|
817
|
+
value: originalName
|
|
818
|
+
},
|
|
819
|
+
fix(fixer) {
|
|
820
|
+
const variable = scope.set.get(aliasName);
|
|
821
|
+
if (!variable) return null;
|
|
822
|
+
if (aliasIsAnnotated) return null;
|
|
823
|
+
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
824
|
+
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
825
|
+
if (readRefs.some((reference) => {
|
|
826
|
+
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
827
|
+
if (afterToken?.value === ":") return false;
|
|
828
|
+
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
829
|
+
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
830
|
+
while (token) {
|
|
831
|
+
if (token.value === "{") return true;
|
|
832
|
+
if (token.value === "[" || token.value === "(") return false;
|
|
833
|
+
if (token.value === ":") return false;
|
|
834
|
+
token = context.sourceCode.getTokenBefore(token);
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
})) return null;
|
|
838
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
839
|
+
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
840
|
+
const declaration = node.parent;
|
|
841
|
+
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
842
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
843
|
+
return fixes;
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
} };
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
//#endregion
|
|
850
|
+
//#region src/rules/no-set-instanceof-mutation.ts
|
|
851
|
+
const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
852
|
+
"add",
|
|
853
|
+
"delete",
|
|
854
|
+
"clear"
|
|
855
|
+
]);
|
|
856
|
+
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
857
|
+
function isSetInstanceofExpression(node) {
|
|
858
|
+
return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Set";
|
|
859
|
+
}
|
|
860
|
+
function definitelyExits(statement) {
|
|
861
|
+
if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
|
|
862
|
+
if (statement.type === AST_NODE_TYPES.BlockStatement) {
|
|
863
|
+
const last = statement.body.at(-1);
|
|
864
|
+
return last !== void 0 && definitelyExits(last);
|
|
865
|
+
}
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
const noSetInstanceofMutation = createRule$2({
|
|
869
|
+
name: "no-set-instanceof-mutation",
|
|
870
|
+
meta: {
|
|
871
|
+
type: "problem",
|
|
872
|
+
schema: [],
|
|
873
|
+
docs: { description: "Disallow mutating calls on a parameter whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
874
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by instanceof Set -- instanceof Set's own narrowing widens straight to the mutable Set interface, so a caller's genuinely read-only set can be mutated here even though the parameter's real type includes a ReadonlySet. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
|
|
875
|
+
},
|
|
876
|
+
defaultOptions: [],
|
|
877
|
+
create(context) {
|
|
878
|
+
const services = ESLintUtils.getParserServices(context);
|
|
879
|
+
const checker = services.program.getTypeChecker();
|
|
880
|
+
function parameterHasReadonlySetConstituent(parameterNode) {
|
|
881
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
882
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
883
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => constituent.getSymbol()?.name === "ReadonlySet");
|
|
884
|
+
}
|
|
885
|
+
return { CallExpression(node) {
|
|
886
|
+
const { callee } = node;
|
|
887
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_SET_METHODS.has(callee.property.name)) return;
|
|
888
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
889
|
+
if (!variable) return;
|
|
890
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
|
|
891
|
+
if (!parameterDefinition) return;
|
|
892
|
+
const parameterNode = parameterDefinition.name;
|
|
893
|
+
if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
894
|
+
if (!parameterHasReadonlySetConstituent(parameterNode)) return;
|
|
895
|
+
if (!isGuardedBySetInstanceof(node, variable, context)) return;
|
|
896
|
+
context.report({
|
|
897
|
+
node,
|
|
898
|
+
messageId: "unsound",
|
|
899
|
+
data: { method: callee.property.name }
|
|
900
|
+
});
|
|
901
|
+
} };
|
|
902
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
903
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
904
|
+
}
|
|
905
|
+
function isNegatedSetInstanceofExpression(testNode, target, ruleContext) {
|
|
906
|
+
if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
907
|
+
return matchesSetInstanceofOn(testNode.argument, target, ruleContext);
|
|
908
|
+
}
|
|
909
|
+
function matchesSetInstanceofOn(testNode, target, ruleContext) {
|
|
910
|
+
if (!isSetInstanceofExpression(testNode)) return false;
|
|
911
|
+
const { left } = testNode;
|
|
912
|
+
return left.type === AST_NODE_TYPES.Identifier && resolvesToVariable(left, target, testNode, ruleContext);
|
|
913
|
+
}
|
|
914
|
+
function isGuardedBySetInstanceof(startNode, parameterVariable, ruleContext) {
|
|
915
|
+
let current = startNode;
|
|
916
|
+
while (current.parent) {
|
|
917
|
+
const { parent } = current;
|
|
918
|
+
if (parent.type === AST_NODE_TYPES.IfStatement) {
|
|
919
|
+
if (parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
920
|
+
if (parent.alternate === current && isNegatedSetInstanceofExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
921
|
+
}
|
|
922
|
+
if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesSetInstanceofOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
923
|
+
if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
924
|
+
current = parent;
|
|
925
|
+
}
|
|
926
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
927
|
+
}
|
|
928
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
929
|
+
let current = startNode;
|
|
930
|
+
while (current.parent) {
|
|
931
|
+
const { parent } = current;
|
|
932
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
|
|
933
|
+
const statements = parent.body;
|
|
934
|
+
let ownIndex = -1;
|
|
935
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
936
|
+
ownIndex = i;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
940
|
+
const sibling = statements[i];
|
|
941
|
+
if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedSetInstanceofExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
current = parent;
|
|
945
|
+
}
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/rules/no-side-effects-in-index.ts
|
|
952
|
+
const noSideEffectsInIndex = {
|
|
953
|
+
meta: {
|
|
954
|
+
type: "problem",
|
|
955
|
+
schema: [],
|
|
956
|
+
messages: { notAPureReexport: "A barrel (index) file may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...') -- nothing else, so it can never have a side effect at import time by construction. Found: {{ description }}." }
|
|
957
|
+
},
|
|
958
|
+
create(context) {
|
|
959
|
+
if (!isIndexFile(context.filename)) return {};
|
|
960
|
+
return { Program(node) {
|
|
961
|
+
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
962
|
+
node: statement,
|
|
963
|
+
messageId: "notAPureReexport",
|
|
964
|
+
data: { description: statement.type }
|
|
965
|
+
});
|
|
966
|
+
} };
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
971
|
+
const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
972
|
+
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
973
|
+
function isDefinitelyNumberType(type) {
|
|
974
|
+
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
975
|
+
return (type.flags & ts.TypeFlags.NumberLike) !== 0;
|
|
976
|
+
}
|
|
977
|
+
const preferNumericSortCompare = createRule$1({
|
|
978
|
+
name: "prefer-numeric-sort-compare",
|
|
979
|
+
meta: {
|
|
980
|
+
type: "suggestion",
|
|
981
|
+
hasSuggestions: true,
|
|
982
|
+
docs: { description: "Suggest an ascending numeric compare function for a bare '.sort()'/'.toSorted()' call on an array whose element type is definitively 'number' -- the default comparator sorts lexicographically, so a bare numeric sort is essentially always a bug." },
|
|
983
|
+
schema: [],
|
|
984
|
+
messages: {
|
|
985
|
+
preferNumericCompare: "'.{{ method }}()' on a number array with no compare function sorts lexicographically (e.g. [1, 2, 10].sort() becomes [1, 10, 2]), not in ascending numeric order. Provide a compare function.",
|
|
986
|
+
addAscendingCompare: "Add an ascending numeric compare function: '(a, b) => a - b'."
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
defaultOptions: [],
|
|
990
|
+
create(context) {
|
|
991
|
+
const services = ESLintUtils.getParserServices(context);
|
|
992
|
+
const checker = services.program.getTypeChecker();
|
|
993
|
+
return { CallExpression(node) {
|
|
994
|
+
if (node.arguments.length > 0) return;
|
|
995
|
+
const { callee } = node;
|
|
996
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return;
|
|
997
|
+
if (callee.property.type !== AST_NODE_TYPES.Identifier || !SORT_METHOD_NAMES.has(callee.property.name)) return;
|
|
998
|
+
const receiverTsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
|
|
999
|
+
if (!ts.isExpression(receiverTsNode)) return;
|
|
1000
|
+
const receiverType = checker.getTypeAtLocation(receiverTsNode);
|
|
1001
|
+
if (!checker.isArrayType(receiverType)) return;
|
|
1002
|
+
if (!isTypeReference(receiverType)) return;
|
|
1003
|
+
const [elementType] = checker.getTypeArguments(receiverType);
|
|
1004
|
+
if (!elementType || !isDefinitelyNumberType(elementType)) return;
|
|
1005
|
+
context.report({
|
|
1006
|
+
node,
|
|
1007
|
+
messageId: "preferNumericCompare",
|
|
1008
|
+
data: { method: callee.property.name },
|
|
1009
|
+
suggest: [{
|
|
1010
|
+
messageId: "addAscendingCompare",
|
|
1011
|
+
fix(fixer) {
|
|
1012
|
+
const closingParen = context.sourceCode.getLastToken(node);
|
|
1013
|
+
if (!closingParen) return null;
|
|
1014
|
+
return fixer.insertTextBefore(closingParen, "(a, b) => a - b");
|
|
1015
|
+
}
|
|
1016
|
+
}]
|
|
1017
|
+
});
|
|
1018
|
+
} };
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/rules/prefer-readonly-array-param.ts
|
|
1023
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1024
|
+
function getFixableArrayOrTupleType(typeNode) {
|
|
1025
|
+
if (typeNode.type === AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
|
|
1026
|
+
if (typeNode.type === AST_NODE_TYPES.TSArrayType) return typeNode;
|
|
1027
|
+
if (typeNode.type === AST_NODE_TYPES.TSTupleType) return typeNode;
|
|
1028
|
+
if (typeNode.type === AST_NODE_TYPES.TSTypeReference && typeNode.typeName.type === AST_NODE_TYPES.Identifier && typeNode.typeName.name === "Array") return typeNode;
|
|
1029
|
+
}
|
|
1030
|
+
function getFixableTypesForAnnotation(typeNode) {
|
|
1031
|
+
if (typeNode.type === AST_NODE_TYPES.TSUnionType) return typeNode.types.flatMap(getFixableTypesForAnnotation);
|
|
1032
|
+
const fixable = getFixableArrayOrTupleType(typeNode);
|
|
1033
|
+
return fixable ? [fixable] : [];
|
|
1034
|
+
}
|
|
1035
|
+
function getAnnotatedParamNode(param) {
|
|
1036
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) return getAnnotatedParamNode(param.parameter);
|
|
1037
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern) return param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0;
|
|
1038
|
+
if (param.type === AST_NODE_TYPES.RestElement || param.type === AST_NODE_TYPES.Identifier) return param;
|
|
1039
|
+
}
|
|
1040
|
+
const FUNCTION_LIKE_SELECTOR = [
|
|
1041
|
+
"ArrowFunctionExpression",
|
|
1042
|
+
"FunctionDeclaration",
|
|
1043
|
+
"FunctionExpression",
|
|
1044
|
+
"TSCallSignatureDeclaration",
|
|
1045
|
+
"TSConstructSignatureDeclaration",
|
|
1046
|
+
"TSDeclareFunction",
|
|
1047
|
+
"TSEmptyBodyFunctionExpression",
|
|
1048
|
+
"TSFunctionType",
|
|
1049
|
+
"TSMethodSignature"
|
|
1050
|
+
].join(", ");
|
|
1051
|
+
const preferReadonlyArrayParam = createRule({
|
|
1052
|
+
name: "prefer-readonly-array-param",
|
|
1053
|
+
meta: {
|
|
1054
|
+
type: "problem",
|
|
1055
|
+
fixable: "code",
|
|
1056
|
+
docs: { description: "Require array and tuple parameters to be typed readonly, regardless of whether the function body mutates them -- a narrower, safely-autofixable sibling of @typescript-eslint/prefer-readonly-parameter-types scoped to array/tuple shapes only." },
|
|
1057
|
+
schema: [],
|
|
1058
|
+
messages: { preferReadonly: "Array and tuple parameters should be typed readonly ({{ suggestion }}) so a caller can pass a readonly or shared array with confidence, and so any mutation inside the function becomes a deliberate, visible compile error instead of a silent side effect on the caller's data." }
|
|
1059
|
+
},
|
|
1060
|
+
defaultOptions: [],
|
|
1061
|
+
create(context) {
|
|
1062
|
+
function checkParam(param) {
|
|
1063
|
+
const annotatedNode = getAnnotatedParamNode(param);
|
|
1064
|
+
if (!annotatedNode?.typeAnnotation) return;
|
|
1065
|
+
const fixableTypes = getFixableTypesForAnnotation(annotatedNode.typeAnnotation.typeAnnotation);
|
|
1066
|
+
if (fixableTypes.length === 0) return;
|
|
1067
|
+
const suggestion = fixableTypes.map((fixableType) => fixableType.type === AST_NODE_TYPES.TSTypeReference ? "ReadonlyArray<T>" : `readonly ${fixableType.type === AST_NODE_TYPES.TSTupleType ? "[T, U]" : "T[]"}`).join(" / ");
|
|
1068
|
+
context.report({
|
|
1069
|
+
node: param,
|
|
1070
|
+
messageId: "preferReadonly",
|
|
1071
|
+
data: { suggestion },
|
|
1072
|
+
fix(fixer) {
|
|
1073
|
+
return fixableTypes.map((fixableType) => fixableType.type === AST_NODE_TYPES.TSTypeReference ? fixer.replaceText(fixableType.typeName, "ReadonlyArray") : fixer.insertTextBefore(fixableType, "readonly "));
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return { [FUNCTION_LIKE_SELECTOR](node) {
|
|
1078
|
+
for (const param of node.params) checkParam(param);
|
|
1079
|
+
} };
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
529
1082
|
//#endregion
|
|
530
1083
|
//#region src/plugin.ts
|
|
531
1084
|
const plugin = {
|
|
@@ -537,83 +1090,20 @@ const plugin = {
|
|
|
537
1090
|
rules: {
|
|
538
1091
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
539
1092
|
"barrel-policy": barrelPolicy,
|
|
1093
|
+
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
540
1094
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
1095
|
+
"no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
|
|
541
1096
|
"no-index-files": noIndexFiles,
|
|
1097
|
+
"no-map-instanceof-mutation": noMapInstanceofMutation,
|
|
542
1098
|
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
543
1099
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
544
1100
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
545
1101
|
"no-object-assign": noObjectAssign,
|
|
546
|
-
"no-pointless-reassignment":
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
552
|
-
},
|
|
553
|
-
create(context) {
|
|
554
|
-
return { VariableDeclarator(node) {
|
|
555
|
-
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
556
|
-
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
557
|
-
const scope = context.sourceCode.getScope(node);
|
|
558
|
-
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
559
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
560
|
-
const aliasName = node.id.name;
|
|
561
|
-
const originalName = node.init.name;
|
|
562
|
-
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
563
|
-
context.report({
|
|
564
|
-
node,
|
|
565
|
-
messageId: "pointlessReassignment",
|
|
566
|
-
data: {
|
|
567
|
-
name: aliasName,
|
|
568
|
-
value: originalName
|
|
569
|
-
},
|
|
570
|
-
fix(fixer) {
|
|
571
|
-
const variable = scope.set.get(aliasName);
|
|
572
|
-
if (!variable) return null;
|
|
573
|
-
if (aliasIsAnnotated) return null;
|
|
574
|
-
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
575
|
-
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
576
|
-
if (readRefs.some((reference) => {
|
|
577
|
-
const afterToken = context.sourceCode.getTokenAfter(reference.identifier);
|
|
578
|
-
if (afterToken?.value === ":") return false;
|
|
579
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
580
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
581
|
-
while (token) {
|
|
582
|
-
if (token.value === "{") return true;
|
|
583
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
584
|
-
if (token.value === ":") return false;
|
|
585
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
586
|
-
}
|
|
587
|
-
return false;
|
|
588
|
-
})) return null;
|
|
589
|
-
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
590
|
-
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
591
|
-
const declaration = node.parent;
|
|
592
|
-
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
593
|
-
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
594
|
-
return fixes;
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
} };
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
"no-side-effects-in-index": {
|
|
601
|
-
meta: {
|
|
602
|
-
type: "problem",
|
|
603
|
-
schema: [],
|
|
604
|
-
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 }}." }
|
|
605
|
-
},
|
|
606
|
-
create(context) {
|
|
607
|
-
if (!isIndexFile(context.filename)) return {};
|
|
608
|
-
return { Program(node) {
|
|
609
|
-
for (const statement of node.body) if (!isPureReexport(statement)) context.report({
|
|
610
|
-
node: statement,
|
|
611
|
-
messageId: "notAPureReexport",
|
|
612
|
-
data: { description: statement.type }
|
|
613
|
-
});
|
|
614
|
-
} };
|
|
615
|
-
}
|
|
616
|
-
}
|
|
1102
|
+
"no-pointless-reassignment": noPointlessReassignment,
|
|
1103
|
+
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1104
|
+
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1105
|
+
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1106
|
+
"prefer-readonly-array-param": preferReadonlyArrayParam
|
|
617
1107
|
},
|
|
618
1108
|
configs: {
|
|
619
1109
|
get recommended() {
|
|
@@ -624,7 +1114,8 @@ const plugin = {
|
|
|
624
1114
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
625
1115
|
"exadev/no-mutable-union-array-param": "error",
|
|
626
1116
|
"exadev/no-object-assign": "error",
|
|
627
|
-
"exadev/no-pointless-reassignment": "error"
|
|
1117
|
+
"exadev/no-pointless-reassignment": "error",
|
|
1118
|
+
"exadev/prefer-readonly-array-param": "error"
|
|
628
1119
|
}
|
|
629
1120
|
};
|
|
630
1121
|
},
|
|
@@ -647,14 +1138,44 @@ const recommendedTypeChecked = [
|
|
|
647
1138
|
linterOptions: { noInlineConfig: true },
|
|
648
1139
|
rules: {
|
|
649
1140
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
1141
|
+
"exadev/no-array-isarray-mutation": "error",
|
|
650
1142
|
"exadev/no-enum-number-widening": "error",
|
|
1143
|
+
"exadev/no-enum-reverse-lookup-widening": "error",
|
|
1144
|
+
"exadev/no-map-instanceof-mutation": "error",
|
|
651
1145
|
"exadev/no-mutable-union-array-param": "error",
|
|
652
1146
|
"exadev/no-object-assign": "error",
|
|
653
1147
|
"exadev/no-pointless-reassignment": "error",
|
|
654
|
-
"
|
|
1148
|
+
"exadev/no-set-instanceof-mutation": "error",
|
|
1149
|
+
"exadev/prefer-numeric-sort-compare": "error",
|
|
1150
|
+
"exadev/prefer-readonly-array-param": "error",
|
|
655
1151
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
1152
|
+
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
1153
|
+
"@typescript-eslint/consistent-type-exports": "error",
|
|
1154
|
+
"@typescript-eslint/consistent-type-imports": "error",
|
|
656
1155
|
"@typescript-eslint/method-signature-style": ["error", "property"],
|
|
657
|
-
"@typescript-eslint/no-
|
|
1156
|
+
"@typescript-eslint/no-deprecated": "error",
|
|
1157
|
+
"@typescript-eslint/no-magic-numbers": ["error", {
|
|
1158
|
+
ignore: [
|
|
1159
|
+
-1,
|
|
1160
|
+
0,
|
|
1161
|
+
1,
|
|
1162
|
+
2
|
|
1163
|
+
],
|
|
1164
|
+
ignoreArrayIndexes: true,
|
|
1165
|
+
ignoreEnums: true,
|
|
1166
|
+
ignoreReadonlyClassProperties: true,
|
|
1167
|
+
ignoreDefaultValues: true
|
|
1168
|
+
}],
|
|
1169
|
+
"@typescript-eslint/no-misused-spread": "error",
|
|
1170
|
+
"@typescript-eslint/no-mixed-enums": "error",
|
|
1171
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
1172
|
+
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
1173
|
+
"@typescript-eslint/prefer-readonly": "error",
|
|
1174
|
+
"@typescript-eslint/promise-function-async": "error",
|
|
1175
|
+
"@typescript-eslint/require-array-sort-compare": "error",
|
|
1176
|
+
"@typescript-eslint/strict-boolean-expressions": "error",
|
|
1177
|
+
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
1178
|
+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error"
|
|
658
1179
|
}
|
|
659
1180
|
},
|
|
660
1181
|
{
|