exadev-eslint-config 2.2.0 → 2.4.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 +3 -1
- package/dist/index.cjs +194 -4
- package/dist/index.js +194 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required pee
|
|
|
16
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-enum-number-widening`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright,
|
|
19
|
+
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/no-deprecated`, `@typescript-eslint/no-misused-spread`, `@typescript-eslint/no-mixed-enums`, `@typescript-eslint/no-unnecessary-condition`, `@typescript-eslint/prefer-readonly`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/use-unknown-in-catch-callback-variable`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), and `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers` is not (see below). Spread it directly into `tseslint.config(...)`:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// eslint.config.ts
|
|
@@ -114,7 +114,9 @@ export default tseslint.config(
|
|
|
114
114
|
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. Autofix rewrites every read to the original name and deletes the declaration (including its `export` keyword, when exported). Still reported but deliberately not auto-fixable where collapsing the alias would change meaning: an explicit type annotation (`const exhaustive: never = item` -- the annotation is the point), a read where the original name is shadowed, a read as a shorthand object property, more than one declarator in the statement, or a source that is written to anywhere. |
|
|
115
115
|
| `no-object-assign` | ✓/suggestion | `Object.assign` does not check a source object's properties against the target's declared types, unlike object spread. A fresh object-literal target autofixes to `{ ...target, ...source }`; mutating an existing reassignable binding offers a suggestion only (changes the object's identity); a `const` binding or a non-statement call site gets a plain report with no fix. |
|
|
116
116
|
| `no-mutable-union-array-param` | ✓ | A function parameter typed as an array of a union (`(string \| number)[]`) accepts a narrower caller array (`number[]`) by covariance; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` on it can then insert a value the caller's own array was never declared to hold. Autofix marks the parameter `readonly`, turning the mutating call into a real compile error to resolve deliberately. Requires no type information. |
|
|
117
|
+
| `no-array-isarray-mutation` | | `Array.isArray`'s own type declaration narrows to plain `any[]`, discarding the `readonly` guarantee of any array type in the narrowed parameter's real type -- a bare `readonly T[]`, a `ReadonlyArray<T>`, one behind a type alias, or one alongside other union members -- inside the guarded branch; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` there can mutate a caller's genuinely readonly array. Recognises the direct `if (Array.isArray(x))` guard (braced or not), the early-return/early-throw idiom, `&&`, the ternary form, and the else-of-a-negated-test form. No autofix: re-adding `readonly` is a no-op (the guard already discarded it) and rewriting the mutating call into a copy-first pattern is not safely mechanical in the presence of aliasing. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended` -- specifically to see through a type alias and to catch a bare, non-union readonly array parameter, neither visible from the parameter's own syntax alone. |
|
|
117
118
|
| `no-enum-number-widening` | | A bare (non-literal) `number` is accepted anywhere a numeric enum is expected, without checking it is actually one of the enum's members -- only a numeric *literal* gets range-checked by `tsc`. No autofix: the only provably safe fix is a genuine runtime membership check against the enum's own values, which is a behavioural choice a mechanical fix cannot responsibly make. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
119
|
+
| `no-enum-reverse-lookup-widening` | suggestion | Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime -- `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
118
120
|
|
|
119
121
|
## Barrel policy
|
|
120
122
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,7 +31,7 @@ let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
|
31
31
|
let typescript = require("typescript");
|
|
32
32
|
typescript = __toESM(typescript, 1);
|
|
33
33
|
//#region package.json
|
|
34
|
-
var version = "2.
|
|
34
|
+
var version = "2.4.0";
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/rules/barrel-helpers.ts
|
|
37
37
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -267,6 +267,109 @@ const barrelPolicy = {
|
|
|
267
267
|
}
|
|
268
268
|
};
|
|
269
269
|
//#endregion
|
|
270
|
+
//#region src/rules/no-array-isarray-mutation.ts
|
|
271
|
+
const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
272
|
+
"push",
|
|
273
|
+
"unshift",
|
|
274
|
+
"splice",
|
|
275
|
+
"fill",
|
|
276
|
+
"copyWithin"
|
|
277
|
+
]);
|
|
278
|
+
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
279
|
+
function isArrayIsArrayCall(node) {
|
|
280
|
+
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && node.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
281
|
+
}
|
|
282
|
+
function definitelyExits(statement) {
|
|
283
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ThrowStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ContinueStatement || statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BreakStatement) return true;
|
|
284
|
+
if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) {
|
|
285
|
+
const last = statement.body.at(-1);
|
|
286
|
+
return last !== void 0 && definitelyExits(last);
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const noArrayIsArrayMutation = createRule$2({
|
|
291
|
+
name: "no-array-isarray-mutation",
|
|
292
|
+
meta: {
|
|
293
|
+
type: "problem",
|
|
294
|
+
schema: [],
|
|
295
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
296
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a 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." }
|
|
297
|
+
},
|
|
298
|
+
defaultOptions: [],
|
|
299
|
+
create(context) {
|
|
300
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
301
|
+
const checker = services.program.getTypeChecker();
|
|
302
|
+
function parameterHasReadonlyArrayConstituent(parameterNode) {
|
|
303
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
304
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
305
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
306
|
+
}
|
|
307
|
+
return { CallExpression(node) {
|
|
308
|
+
const { callee } = node;
|
|
309
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
310
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
311
|
+
if (!variable) return;
|
|
312
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
|
|
313
|
+
if (!parameterDefinition) return;
|
|
314
|
+
const parameterNode = parameterDefinition.name;
|
|
315
|
+
if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
316
|
+
if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
|
|
317
|
+
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
318
|
+
context.report({
|
|
319
|
+
node,
|
|
320
|
+
messageId: "unsound",
|
|
321
|
+
data: { method: callee.property.name }
|
|
322
|
+
});
|
|
323
|
+
} };
|
|
324
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
325
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
326
|
+
}
|
|
327
|
+
function isNegatedArrayIsArrayCall(testNode, target, ruleContext) {
|
|
328
|
+
if (testNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
329
|
+
return matchesArrayIsArrayOn(testNode.argument, target, ruleContext);
|
|
330
|
+
}
|
|
331
|
+
function matchesArrayIsArrayOn(testNode, target, ruleContext) {
|
|
332
|
+
if (!isArrayIsArrayCall(testNode)) return false;
|
|
333
|
+
const [argument] = testNode.arguments;
|
|
334
|
+
return argument?.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && resolvesToVariable(argument, target, testNode, ruleContext);
|
|
335
|
+
}
|
|
336
|
+
function isGuardedByArrayIsArray(startNode, parameterVariable, ruleContext) {
|
|
337
|
+
let current = startNode;
|
|
338
|
+
while (current.parent) {
|
|
339
|
+
const { parent } = current;
|
|
340
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement) {
|
|
341
|
+
if (parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
342
|
+
if (parent.alternate === current && isNegatedArrayIsArrayCall(parent.test, parameterVariable, ruleContext)) return true;
|
|
343
|
+
}
|
|
344
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesArrayIsArrayOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
345
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
346
|
+
current = parent;
|
|
347
|
+
}
|
|
348
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
349
|
+
}
|
|
350
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
351
|
+
let current = startNode;
|
|
352
|
+
while (current.parent) {
|
|
353
|
+
const { parent } = current;
|
|
354
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
355
|
+
const statements = parent.body;
|
|
356
|
+
let ownIndex = -1;
|
|
357
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
358
|
+
ownIndex = i;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
362
|
+
const sibling = statements[i];
|
|
363
|
+
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
current = parent;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
//#endregion
|
|
270
373
|
//#region src/rules/no-enum-number-widening.ts
|
|
271
374
|
const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
272
375
|
name: "no-enum-number-widening",
|
|
@@ -312,6 +415,67 @@ const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((n
|
|
|
312
415
|
}
|
|
313
416
|
});
|
|
314
417
|
//#endregion
|
|
418
|
+
//#region src/rules/no-enum-reverse-lookup-widening.ts
|
|
419
|
+
const noEnumReverseLookupWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
420
|
+
name: "no-enum-reverse-lookup-widening",
|
|
421
|
+
meta: {
|
|
422
|
+
type: "problem",
|
|
423
|
+
hasSuggestions: true,
|
|
424
|
+
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." },
|
|
425
|
+
schema: [],
|
|
426
|
+
messages: {
|
|
427
|
+
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.",
|
|
428
|
+
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."
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
defaultOptions: [],
|
|
432
|
+
create(context) {
|
|
433
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
434
|
+
const checker = services.program.getTypeChecker();
|
|
435
|
+
return { MemberExpression(node) {
|
|
436
|
+
if (!node.computed) return;
|
|
437
|
+
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
|
|
438
|
+
if (!typescript.isExpression(objectTsNode)) return;
|
|
439
|
+
const objectType = checker.getTypeAtLocation(objectTsNode);
|
|
440
|
+
const objectSymbol = objectType.getSymbol();
|
|
441
|
+
if (!objectSymbol || !(objectSymbol.flags & typescript.SymbolFlags.Enum)) return;
|
|
442
|
+
if (!checker.getIndexInfoOfType(objectType, typescript.IndexKind.Number)) return;
|
|
443
|
+
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(node.property);
|
|
444
|
+
if (!typescript.isExpression(propertyTsNode)) return;
|
|
445
|
+
const rawPropertyType = checker.getTypeAtLocation(propertyTsNode);
|
|
446
|
+
const propertyType = checker.getBaseConstraintOfType(rawPropertyType) ?? rawPropertyType;
|
|
447
|
+
if (propertyType.flags & typescript.TypeFlags.EnumLike) {
|
|
448
|
+
if (checker.isTypeAssignableTo(propertyType, checker.getDeclaredTypeOfSymbol(objectSymbol))) return;
|
|
449
|
+
} else {
|
|
450
|
+
if (propertyType.isLiteral()) return;
|
|
451
|
+
if (!(propertyType.flags & typescript.TypeFlags.NumberLike)) return;
|
|
452
|
+
}
|
|
453
|
+
const enumName = checker.typeToString(checker.getDeclaredTypeOfSymbol(objectSymbol));
|
|
454
|
+
const parent = node.parent;
|
|
455
|
+
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) {
|
|
456
|
+
const stringKeyword = parent.id.typeAnnotation.typeAnnotation;
|
|
457
|
+
context.report({
|
|
458
|
+
node,
|
|
459
|
+
messageId: "widening",
|
|
460
|
+
data: { enumName },
|
|
461
|
+
suggest: [{
|
|
462
|
+
messageId: "suggestWidenAnnotation",
|
|
463
|
+
fix(fixer) {
|
|
464
|
+
return fixer.replaceText(stringKeyword, "string | undefined");
|
|
465
|
+
}
|
|
466
|
+
}]
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
context.report({
|
|
471
|
+
node,
|
|
472
|
+
messageId: "widening",
|
|
473
|
+
data: { enumName }
|
|
474
|
+
});
|
|
475
|
+
} };
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
//#endregion
|
|
315
479
|
//#region src/rules/no-index-files.ts
|
|
316
480
|
const noIndexFiles = {
|
|
317
481
|
meta: {
|
|
@@ -565,7 +729,9 @@ const plugin = {
|
|
|
565
729
|
rules: {
|
|
566
730
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
567
731
|
"barrel-policy": barrelPolicy,
|
|
732
|
+
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
568
733
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
734
|
+
"no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
|
|
569
735
|
"no-index-files": noIndexFiles,
|
|
570
736
|
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
571
737
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
@@ -584,7 +750,7 @@ const plugin = {
|
|
|
584
750
|
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
585
751
|
const scope = context.sourceCode.getScope(node);
|
|
586
752
|
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
587
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() &&
|
|
753
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
588
754
|
const aliasName = node.id.name;
|
|
589
755
|
const originalName = node.init.name;
|
|
590
756
|
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
@@ -675,13 +841,37 @@ const recommendedTypeChecked = [
|
|
|
675
841
|
linterOptions: { noInlineConfig: true },
|
|
676
842
|
rules: {
|
|
677
843
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
844
|
+
"exadev/no-array-isarray-mutation": "error",
|
|
678
845
|
"exadev/no-enum-number-widening": "error",
|
|
846
|
+
"exadev/no-enum-reverse-lookup-widening": "error",
|
|
679
847
|
"exadev/no-mutable-union-array-param": "error",
|
|
680
848
|
"exadev/no-object-assign": "error",
|
|
681
849
|
"exadev/no-pointless-reassignment": "error",
|
|
682
|
-
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
683
850
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
684
|
-
"@typescript-eslint/
|
|
851
|
+
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
852
|
+
"@typescript-eslint/method-signature-style": ["error", "property"],
|
|
853
|
+
"@typescript-eslint/no-deprecated": "error",
|
|
854
|
+
"@typescript-eslint/no-magic-numbers": ["error", {
|
|
855
|
+
ignore: [
|
|
856
|
+
-1,
|
|
857
|
+
0,
|
|
858
|
+
1,
|
|
859
|
+
2
|
|
860
|
+
],
|
|
861
|
+
ignoreArrayIndexes: true,
|
|
862
|
+
ignoreEnums: true,
|
|
863
|
+
ignoreReadonlyClassProperties: true,
|
|
864
|
+
ignoreDefaultValues: true
|
|
865
|
+
}],
|
|
866
|
+
"@typescript-eslint/no-misused-spread": "error",
|
|
867
|
+
"@typescript-eslint/no-mixed-enums": "error",
|
|
868
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
869
|
+
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
870
|
+
"@typescript-eslint/prefer-readonly": "error",
|
|
871
|
+
"@typescript-eslint/require-array-sort-compare": "error",
|
|
872
|
+
"@typescript-eslint/strict-boolean-expressions": "error",
|
|
873
|
+
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
874
|
+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error"
|
|
685
875
|
}
|
|
686
876
|
},
|
|
687
877
|
{
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
5
|
//#region package.json
|
|
6
|
-
var version = "2.
|
|
6
|
+
var version = "2.4.0";
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/rules/barrel-helpers.ts
|
|
9
9
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -239,6 +239,109 @@ const barrelPolicy = {
|
|
|
239
239
|
}
|
|
240
240
|
};
|
|
241
241
|
//#endregion
|
|
242
|
+
//#region src/rules/no-array-isarray-mutation.ts
|
|
243
|
+
const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
244
|
+
"push",
|
|
245
|
+
"unshift",
|
|
246
|
+
"splice",
|
|
247
|
+
"fill",
|
|
248
|
+
"copyWithin"
|
|
249
|
+
]);
|
|
250
|
+
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
251
|
+
function isArrayIsArrayCall(node) {
|
|
252
|
+
return node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
253
|
+
}
|
|
254
|
+
function definitelyExits(statement) {
|
|
255
|
+
if (statement.type === AST_NODE_TYPES.ReturnStatement || statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ContinueStatement || statement.type === AST_NODE_TYPES.BreakStatement) return true;
|
|
256
|
+
if (statement.type === AST_NODE_TYPES.BlockStatement) {
|
|
257
|
+
const last = statement.body.at(-1);
|
|
258
|
+
return last !== void 0 && definitelyExits(last);
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const noArrayIsArrayMutation = createRule$2({
|
|
263
|
+
name: "no-array-isarray-mutation",
|
|
264
|
+
meta: {
|
|
265
|
+
type: "problem",
|
|
266
|
+
schema: [],
|
|
267
|
+
docs: { description: "Disallow mutating-insertion calls on a parameter whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
268
|
+
messages: { unsound: "'{{ method }}' mutates a parameter narrowed by Array.isArray -- Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a caller's genuinely readonly array can be mutated here even though the parameter's real type includes a readonly array. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
269
|
+
},
|
|
270
|
+
defaultOptions: [],
|
|
271
|
+
create(context) {
|
|
272
|
+
const services = ESLintUtils.getParserServices(context);
|
|
273
|
+
const checker = services.program.getTypeChecker();
|
|
274
|
+
function parameterHasReadonlyArrayConstituent(parameterNode) {
|
|
275
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(parameterNode);
|
|
276
|
+
const parameterType = checker.getTypeAtLocation(tsNode);
|
|
277
|
+
return (parameterType.isUnion() ? parameterType.types : [parameterType]).some((constituent) => checker.isArrayType(constituent) && constituent.getSymbol()?.name === "ReadonlyArray");
|
|
278
|
+
}
|
|
279
|
+
return { CallExpression(node) {
|
|
280
|
+
const { callee } = node;
|
|
281
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
282
|
+
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
283
|
+
if (!variable) return;
|
|
284
|
+
const parameterDefinition = variable.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
|
|
285
|
+
if (!parameterDefinition) return;
|
|
286
|
+
const parameterNode = parameterDefinition.name;
|
|
287
|
+
if (parameterNode.type !== AST_NODE_TYPES.Identifier) return;
|
|
288
|
+
if (!parameterHasReadonlyArrayConstituent(parameterNode)) return;
|
|
289
|
+
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
290
|
+
context.report({
|
|
291
|
+
node,
|
|
292
|
+
messageId: "unsound",
|
|
293
|
+
data: { method: callee.property.name }
|
|
294
|
+
});
|
|
295
|
+
} };
|
|
296
|
+
function resolvesToVariable(identifier, target, atNode, ruleContext) {
|
|
297
|
+
return ruleContext.sourceCode.getScope(atNode).references.find((reference) => reference.identifier === identifier)?.resolved === target;
|
|
298
|
+
}
|
|
299
|
+
function isNegatedArrayIsArrayCall(testNode, target, ruleContext) {
|
|
300
|
+
if (testNode.type !== AST_NODE_TYPES.UnaryExpression || testNode.operator !== "!") return false;
|
|
301
|
+
return matchesArrayIsArrayOn(testNode.argument, target, ruleContext);
|
|
302
|
+
}
|
|
303
|
+
function matchesArrayIsArrayOn(testNode, target, ruleContext) {
|
|
304
|
+
if (!isArrayIsArrayCall(testNode)) return false;
|
|
305
|
+
const [argument] = testNode.arguments;
|
|
306
|
+
return argument?.type === AST_NODE_TYPES.Identifier && resolvesToVariable(argument, target, testNode, ruleContext);
|
|
307
|
+
}
|
|
308
|
+
function isGuardedByArrayIsArray(startNode, parameterVariable, ruleContext) {
|
|
309
|
+
let current = startNode;
|
|
310
|
+
while (current.parent) {
|
|
311
|
+
const { parent } = current;
|
|
312
|
+
if (parent.type === AST_NODE_TYPES.IfStatement) {
|
|
313
|
+
if (parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
314
|
+
if (parent.alternate === current && isNegatedArrayIsArrayCall(parent.test, parameterVariable, ruleContext)) return true;
|
|
315
|
+
}
|
|
316
|
+
if (parent.type === AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && parent.right === current && matchesArrayIsArrayOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
317
|
+
if (parent.type === AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
318
|
+
current = parent;
|
|
319
|
+
}
|
|
320
|
+
return isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext);
|
|
321
|
+
}
|
|
322
|
+
function isGuardedByPrecedingEarlyReturn(startNode, parameterVariable, ruleContext) {
|
|
323
|
+
let current = startNode;
|
|
324
|
+
while (current.parent) {
|
|
325
|
+
const { parent } = current;
|
|
326
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) {
|
|
327
|
+
const statements = parent.body;
|
|
328
|
+
let ownIndex = -1;
|
|
329
|
+
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
330
|
+
ownIndex = i;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
334
|
+
const sibling = statements[i];
|
|
335
|
+
if (sibling?.type === AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
current = parent;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
//#endregion
|
|
242
345
|
//#region src/rules/no-enum-number-widening.ts
|
|
243
346
|
const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
244
347
|
name: "no-enum-number-widening",
|
|
@@ -284,6 +387,67 @@ const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.c
|
|
|
284
387
|
}
|
|
285
388
|
});
|
|
286
389
|
//#endregion
|
|
390
|
+
//#region src/rules/no-enum-reverse-lookup-widening.ts
|
|
391
|
+
const noEnumReverseLookupWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
392
|
+
name: "no-enum-reverse-lookup-widening",
|
|
393
|
+
meta: {
|
|
394
|
+
type: "problem",
|
|
395
|
+
hasSuggestions: true,
|
|
396
|
+
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." },
|
|
397
|
+
schema: [],
|
|
398
|
+
messages: {
|
|
399
|
+
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.",
|
|
400
|
+
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."
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
defaultOptions: [],
|
|
404
|
+
create(context) {
|
|
405
|
+
const services = ESLintUtils.getParserServices(context);
|
|
406
|
+
const checker = services.program.getTypeChecker();
|
|
407
|
+
return { MemberExpression(node) {
|
|
408
|
+
if (!node.computed) return;
|
|
409
|
+
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
|
|
410
|
+
if (!ts.isExpression(objectTsNode)) return;
|
|
411
|
+
const objectType = checker.getTypeAtLocation(objectTsNode);
|
|
412
|
+
const objectSymbol = objectType.getSymbol();
|
|
413
|
+
if (!objectSymbol || !(objectSymbol.flags & ts.SymbolFlags.Enum)) return;
|
|
414
|
+
if (!checker.getIndexInfoOfType(objectType, ts.IndexKind.Number)) return;
|
|
415
|
+
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(node.property);
|
|
416
|
+
if (!ts.isExpression(propertyTsNode)) return;
|
|
417
|
+
const rawPropertyType = checker.getTypeAtLocation(propertyTsNode);
|
|
418
|
+
const propertyType = checker.getBaseConstraintOfType(rawPropertyType) ?? rawPropertyType;
|
|
419
|
+
if (propertyType.flags & ts.TypeFlags.EnumLike) {
|
|
420
|
+
if (checker.isTypeAssignableTo(propertyType, checker.getDeclaredTypeOfSymbol(objectSymbol))) return;
|
|
421
|
+
} else {
|
|
422
|
+
if (propertyType.isLiteral()) return;
|
|
423
|
+
if (!(propertyType.flags & ts.TypeFlags.NumberLike)) return;
|
|
424
|
+
}
|
|
425
|
+
const enumName = checker.typeToString(checker.getDeclaredTypeOfSymbol(objectSymbol));
|
|
426
|
+
const parent = node.parent;
|
|
427
|
+
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) {
|
|
428
|
+
const stringKeyword = parent.id.typeAnnotation.typeAnnotation;
|
|
429
|
+
context.report({
|
|
430
|
+
node,
|
|
431
|
+
messageId: "widening",
|
|
432
|
+
data: { enumName },
|
|
433
|
+
suggest: [{
|
|
434
|
+
messageId: "suggestWidenAnnotation",
|
|
435
|
+
fix(fixer) {
|
|
436
|
+
return fixer.replaceText(stringKeyword, "string | undefined");
|
|
437
|
+
}
|
|
438
|
+
}]
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
context.report({
|
|
443
|
+
node,
|
|
444
|
+
messageId: "widening",
|
|
445
|
+
data: { enumName }
|
|
446
|
+
});
|
|
447
|
+
} };
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
//#endregion
|
|
287
451
|
//#region src/rules/no-index-files.ts
|
|
288
452
|
const noIndexFiles = {
|
|
289
453
|
meta: {
|
|
@@ -537,7 +701,9 @@ const plugin = {
|
|
|
537
701
|
rules: {
|
|
538
702
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
539
703
|
"barrel-policy": barrelPolicy,
|
|
704
|
+
"no-array-isarray-mutation": noArrayIsArrayMutation,
|
|
540
705
|
"no-enum-number-widening": noEnumNumberWidening,
|
|
706
|
+
"no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
|
|
541
707
|
"no-index-files": noIndexFiles,
|
|
542
708
|
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
543
709
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
@@ -556,7 +722,7 @@ const plugin = {
|
|
|
556
722
|
if (node.parent.type !== "VariableDeclaration" || node.parent.kind !== "const") return;
|
|
557
723
|
const scope = context.sourceCode.getScope(node);
|
|
558
724
|
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
559
|
-
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() &&
|
|
725
|
+
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
560
726
|
const aliasName = node.id.name;
|
|
561
727
|
const originalName = node.init.name;
|
|
562
728
|
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
@@ -647,13 +813,37 @@ const recommendedTypeChecked = [
|
|
|
647
813
|
linterOptions: { noInlineConfig: true },
|
|
648
814
|
rules: {
|
|
649
815
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
816
|
+
"exadev/no-array-isarray-mutation": "error",
|
|
650
817
|
"exadev/no-enum-number-widening": "error",
|
|
818
|
+
"exadev/no-enum-reverse-lookup-widening": "error",
|
|
651
819
|
"exadev/no-mutable-union-array-param": "error",
|
|
652
820
|
"exadev/no-object-assign": "error",
|
|
653
821
|
"exadev/no-pointless-reassignment": "error",
|
|
654
|
-
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
655
822
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
656
|
-
"@typescript-eslint/
|
|
823
|
+
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
824
|
+
"@typescript-eslint/method-signature-style": ["error", "property"],
|
|
825
|
+
"@typescript-eslint/no-deprecated": "error",
|
|
826
|
+
"@typescript-eslint/no-magic-numbers": ["error", {
|
|
827
|
+
ignore: [
|
|
828
|
+
-1,
|
|
829
|
+
0,
|
|
830
|
+
1,
|
|
831
|
+
2
|
|
832
|
+
],
|
|
833
|
+
ignoreArrayIndexes: true,
|
|
834
|
+
ignoreEnums: true,
|
|
835
|
+
ignoreReadonlyClassProperties: true,
|
|
836
|
+
ignoreDefaultValues: true
|
|
837
|
+
}],
|
|
838
|
+
"@typescript-eslint/no-misused-spread": "error",
|
|
839
|
+
"@typescript-eslint/no-mixed-enums": "error",
|
|
840
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
841
|
+
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
842
|
+
"@typescript-eslint/prefer-readonly": "error",
|
|
843
|
+
"@typescript-eslint/require-array-sort-compare": "error",
|
|
844
|
+
"@typescript-eslint/strict-boolean-expressions": "error",
|
|
845
|
+
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
846
|
+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error"
|
|
657
847
|
}
|
|
658
848
|
},
|
|
659
849
|
{
|