exadev-eslint-config 2.1.1 → 2.2.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 -4
- package/dist/index.cjs +194 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +193 -3
- package/package.json +7 -2
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-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions,
|
|
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, and `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound) -- the type-assertion and ts-comment rules are relaxed in test files (see below). Spread it directly into `tseslint.config(...)`:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// eslint.config.ts
|
|
@@ -41,7 +41,7 @@ export default tseslint.config(
|
|
|
41
41
|
{ rules: { 'exadev/barrel-policy': ['error', { mode: 'single' }] } }, // this package keeps its barrel
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`recommendedTypeChecked` subsumes typescript-eslint's plain `recommended` outright (
|
|
44
|
+
`recommendedTypeChecked` subsumes typescript-eslint's plain `recommended` outright (every rule in `recommended` is also present in `recommendedTypeChecked`), and its base config registers the `@typescript-eslint` plugin and sets `languageOptions.parser` itself. That is why **you must remove your own `...tseslint.configs.recommended`/`recommendedTypeChecked`/`stylisticTypeChecked` spreads** -- flat config rejects two different plugin object instances registered under the same namespace. You still supply `languageOptions.parserOptions.project`/`projectService` pointing at your own tsconfig(s).
|
|
45
45
|
|
|
46
46
|
**Test files (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions, and only those two.** `@ts-expect-error` reverts to `allow-with-description` (a compile-time-only assertion of a type failure is a legitimate test pattern; `@ts-ignore`/`@ts-nocheck` stay banned since `@ts-expect-error` is strictly better). `consistent-type-assertions` relaxes to `assertionStyle: 'as'` (the legacy `<Type>value` form stays banned everywhere). Nothing inherited from the presets is relaxed.
|
|
47
47
|
|
|
@@ -77,7 +77,7 @@ export default defineConfig([
|
|
|
77
77
|
{
|
|
78
78
|
files: ['**/*.ts'],
|
|
79
79
|
plugins: { exadev: plugin },
|
|
80
|
-
extends: ['exadev/recommended'], // this plugin's own
|
|
80
|
+
extends: ['exadev/recommended'], // this plugin's own non-type-aware rules, plus linterOptions.noInlineConfig -- no type-checked rules at all
|
|
81
81
|
// or: extends: ['exadev/barrel'], // just the barrel-discipline trio (no-non-barrel-index, no-non-barrel-reexport, no-side-effects-in-index)
|
|
82
82
|
},
|
|
83
83
|
]);
|
|
@@ -111,7 +111,10 @@ export default tseslint.config(
|
|
|
111
111
|
| `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel. Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match. Autofix deletes the export and the now-pointless import when it was the import's only use. Self-scopes away from any index file. |
|
|
112
112
|
| `no-side-effects-in-index` | | A barrel file may contain only re-export statements -- nothing that could execute at import time. Self-scopes to any index file. |
|
|
113
113
|
| `barrel-direct-siblings-only` | | A barrel may re-export only from a direct sibling (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
114
|
-
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. |
|
|
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
|
+
| `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
|
+
| `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-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`. |
|
|
115
118
|
|
|
116
119
|
## Barrel policy
|
|
117
120
|
|
package/dist/index.cjs
CHANGED
|
@@ -27,8 +27,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
let typescript_eslint = require("typescript-eslint");
|
|
28
28
|
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
29
29
|
let node_path = require("node:path");
|
|
30
|
+
let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
31
|
+
let typescript = require("typescript");
|
|
32
|
+
typescript = __toESM(typescript, 1);
|
|
30
33
|
//#region package.json
|
|
31
|
-
var version = "2.
|
|
34
|
+
var version = "2.2.0";
|
|
32
35
|
//#endregion
|
|
33
36
|
//#region src/rules/barrel-helpers.ts
|
|
34
37
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -264,6 +267,51 @@ const barrelPolicy = {
|
|
|
264
267
|
}
|
|
265
268
|
};
|
|
266
269
|
//#endregion
|
|
270
|
+
//#region src/rules/no-enum-number-widening.ts
|
|
271
|
+
const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
272
|
+
name: "no-enum-number-widening",
|
|
273
|
+
meta: {
|
|
274
|
+
type: "problem",
|
|
275
|
+
docs: { description: "Disallow assigning a bare (non-literal) number where a numeric enum type is expected -- TypeScript accepts any number for a numeric enum slot, not just its own members, once the value is not a literal the compiler can range-check." },
|
|
276
|
+
schema: [],
|
|
277
|
+
messages: { widening: "A plain 'number' value is being used where the numeric enum '{{ enumName }}' is expected. TypeScript does not verify the value is actually one of the enum's members here -- narrow it to a known member first (e.g. a lookup/guard against the enum's own values), or accept a plain 'number' parameter instead of widening it implicitly." }
|
|
278
|
+
},
|
|
279
|
+
defaultOptions: [],
|
|
280
|
+
create(context) {
|
|
281
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
282
|
+
const checker = services.program.getTypeChecker();
|
|
283
|
+
function checkNode(expression) {
|
|
284
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(expression);
|
|
285
|
+
if (!typescript.isExpression(tsNode)) return;
|
|
286
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
287
|
+
if (!contextualType || !(contextualType.flags & typescript.TypeFlags.EnumLike)) return;
|
|
288
|
+
const actualType = checker.getTypeAtLocation(tsNode);
|
|
289
|
+
if (actualType.flags & typescript.TypeFlags.EnumLike) return;
|
|
290
|
+
if (actualType.isLiteral()) return;
|
|
291
|
+
if (!(actualType.flags & typescript.TypeFlags.NumberLike)) return;
|
|
292
|
+
context.report({
|
|
293
|
+
node: expression,
|
|
294
|
+
messageId: "widening",
|
|
295
|
+
data: { enumName: checker.typeToString(contextualType) }
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
VariableDeclarator(node) {
|
|
300
|
+
if (node.init) checkNode(node.init);
|
|
301
|
+
},
|
|
302
|
+
AssignmentExpression(node) {
|
|
303
|
+
checkNode(node.right);
|
|
304
|
+
},
|
|
305
|
+
ReturnStatement(node) {
|
|
306
|
+
if (node.argument) checkNode(node.argument);
|
|
307
|
+
},
|
|
308
|
+
CallExpression(node) {
|
|
309
|
+
for (const argument of node.arguments) if (argument.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) checkNode(argument);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
//#endregion
|
|
267
315
|
//#region src/rules/no-index-files.ts
|
|
268
316
|
const noIndexFiles = {
|
|
269
317
|
meta: {
|
|
@@ -282,6 +330,58 @@ const noIndexFiles = {
|
|
|
282
330
|
}
|
|
283
331
|
};
|
|
284
332
|
//#endregion
|
|
333
|
+
//#region src/rules/no-mutable-union-array-param.ts
|
|
334
|
+
const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
335
|
+
"push",
|
|
336
|
+
"unshift",
|
|
337
|
+
"splice",
|
|
338
|
+
"fill",
|
|
339
|
+
"copyWithin"
|
|
340
|
+
]);
|
|
341
|
+
const createRule$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
342
|
+
function isUnionArrayType(typeAnnotation) {
|
|
343
|
+
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
344
|
+
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) {
|
|
345
|
+
const firstParam = typeAnnotation.typeArguments.params[0];
|
|
346
|
+
if (firstParam?.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const noMutableUnionArrayParam = createRule$1({
|
|
350
|
+
name: "no-mutable-union-array-param",
|
|
351
|
+
meta: {
|
|
352
|
+
type: "problem",
|
|
353
|
+
fixable: "code",
|
|
354
|
+
docs: { description: "Disallow mutating-insertion calls on a union-element array parameter, which lets a caller pass a narrower array whose declared element type the call can silently violate." },
|
|
355
|
+
schema: [],
|
|
356
|
+
messages: { unsound: "'{{ method }}' inserts into a parameter typed as an array of a union -- a caller may have passed a narrower array (e.g. number[] where (string | number)[] is declared), and TypeScript's covariant array typing does not catch the resulting mismatch. Mark the parameter readonly to turn this into a real compile error, or narrow the parameter type." }
|
|
357
|
+
},
|
|
358
|
+
defaultOptions: [],
|
|
359
|
+
create(context) {
|
|
360
|
+
return { CallExpression(node) {
|
|
361
|
+
const { callee } = node;
|
|
362
|
+
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.has(callee.property.name)) return;
|
|
363
|
+
const parameterDefinition = (context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved)?.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter);
|
|
364
|
+
if (!parameterDefinition) return;
|
|
365
|
+
const parameterNode = parameterDefinition.name;
|
|
366
|
+
if (parameterNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !parameterNode.typeAnnotation) return;
|
|
367
|
+
if (!isUnionArrayType(parameterNode.typeAnnotation.typeAnnotation)) return;
|
|
368
|
+
context.report({
|
|
369
|
+
node,
|
|
370
|
+
messageId: "unsound",
|
|
371
|
+
data: { method: callee.property.name },
|
|
372
|
+
fix(fixer) {
|
|
373
|
+
const typeAnnotation = parameterNode.typeAnnotation;
|
|
374
|
+
if (!typeAnnotation) return null;
|
|
375
|
+
const annotated = typeAnnotation.typeAnnotation;
|
|
376
|
+
if (annotated.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
377
|
+
if (annotated.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference && annotated.typeName.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return fixer.replaceText(annotated.typeName, "ReadonlyArray");
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
} };
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
//#endregion
|
|
285
385
|
//#region src/rules/no-non-barrel-index.ts
|
|
286
386
|
const INDEX_BASENAME = /^index\.[cm]?[tj]s$/;
|
|
287
387
|
const noNonBarrelIndex = {
|
|
@@ -371,10 +471,89 @@ const noNonBarrelReexport = {
|
|
|
371
471
|
}
|
|
372
472
|
};
|
|
373
473
|
//#endregion
|
|
474
|
+
//#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`);
|
|
476
|
+
function resolveFrom$1(scope, name) {
|
|
477
|
+
for (let current = scope; current; current = current.upper) {
|
|
478
|
+
const found = current.set.get(name);
|
|
479
|
+
if (found) return found;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const noObjectAssign = createRule({
|
|
483
|
+
name: "no-object-assign",
|
|
484
|
+
meta: {
|
|
485
|
+
type: "problem",
|
|
486
|
+
fixable: "code",
|
|
487
|
+
hasSuggestions: true,
|
|
488
|
+
docs: { description: "Disallow Object.assign, whose own type declarations do not check a source object's properties against the target's declared types -- object spread does." },
|
|
489
|
+
schema: [],
|
|
490
|
+
messages: {
|
|
491
|
+
unsound: "Object.assign does not verify that a source object's properties are assignable to the target's declared types, so a type mismatch here passes silently where a direct property assignment would be rejected. Use object spread ({ ...target, ...source }) to build a correctly type-checked replacement instead.",
|
|
492
|
+
suggestSpreadReassign: "Replace with object spread and reassignment (changes the object reference -- anything else already holding this object will not see the update)."
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
defaultOptions: [],
|
|
496
|
+
create(context) {
|
|
497
|
+
return { CallExpression(node) {
|
|
498
|
+
const { callee } = node;
|
|
499
|
+
if (!(callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && callee.object.name === "Object" && callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && callee.property.name === "assign")) return;
|
|
500
|
+
const [target, ...sources] = node.arguments;
|
|
501
|
+
const sourcesAreSpreadable = sources.every((argument) => argument.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement);
|
|
502
|
+
if (target?.type === _typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression && sourcesAreSpreadable) {
|
|
503
|
+
const isBareStatement = node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement && node.parent.expression === node;
|
|
504
|
+
context.report({
|
|
505
|
+
node,
|
|
506
|
+
messageId: "unsound",
|
|
507
|
+
fix(fixer) {
|
|
508
|
+
const targetProps = target.properties.map((property) => context.sourceCode.getText(property));
|
|
509
|
+
const sourceSpreads = sources.map((argument) => `...${context.sourceCode.getText(argument)}`);
|
|
510
|
+
const objectLiteral = `{ ${[...targetProps, ...sourceSpreads].join(", ")} }`;
|
|
511
|
+
return fixer.replaceText(node, isBareStatement ? `(${objectLiteral})` : objectLiteral);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (target?.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && sourcesAreSpreadable && node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement && node.parent.expression === node) {
|
|
517
|
+
const targetName = target.name;
|
|
518
|
+
const variable = resolveFrom$1(context.sourceCode.getScope(node), targetName);
|
|
519
|
+
const isConst = (variable?.defs.find((candidate) => candidate.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable))?.parent.kind === "const";
|
|
520
|
+
const statement = node.parent;
|
|
521
|
+
if (variable && !isConst) {
|
|
522
|
+
context.report({
|
|
523
|
+
node,
|
|
524
|
+
messageId: "unsound",
|
|
525
|
+
suggest: [{
|
|
526
|
+
messageId: "suggestSpreadReassign",
|
|
527
|
+
fix(fixer) {
|
|
528
|
+
const sourceSpreads = sources.map((argument) => `...${context.sourceCode.getText(argument)}`);
|
|
529
|
+
return fixer.replaceText(statement, `${targetName} = { ...${targetName}, ${sourceSpreads.join(", ")} };`);
|
|
530
|
+
}
|
|
531
|
+
}]
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
context.report({
|
|
537
|
+
node,
|
|
538
|
+
messageId: "unsound"
|
|
539
|
+
});
|
|
540
|
+
} };
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
//#endregion
|
|
374
544
|
//#region src/rules/no-pointless-reassignment.ts
|
|
375
545
|
function isIdentifierReference(reference) {
|
|
376
546
|
return reference.identifier.type === "Identifier";
|
|
377
547
|
}
|
|
548
|
+
function hasTypeAnnotation(id) {
|
|
549
|
+
return "typeAnnotation" in id && id.typeAnnotation !== void 0 && id.typeAnnotation !== null;
|
|
550
|
+
}
|
|
551
|
+
function resolveFrom(scope, name) {
|
|
552
|
+
for (let current = scope; current; current = current.upper) {
|
|
553
|
+
const found = current.set.get(name);
|
|
554
|
+
if (found) return found;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
378
557
|
//#endregion
|
|
379
558
|
//#region src/plugin.ts
|
|
380
559
|
const plugin = {
|
|
@@ -386,9 +565,12 @@ const plugin = {
|
|
|
386
565
|
rules: {
|
|
387
566
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
388
567
|
"barrel-policy": barrelPolicy,
|
|
568
|
+
"no-enum-number-widening": noEnumNumberWidening,
|
|
389
569
|
"no-index-files": noIndexFiles,
|
|
570
|
+
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
390
571
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
391
572
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
573
|
+
"no-object-assign": noObjectAssign,
|
|
392
574
|
"no-pointless-reassignment": {
|
|
393
575
|
meta: {
|
|
394
576
|
type: "problem",
|
|
@@ -405,6 +587,7 @@ const plugin = {
|
|
|
405
587
|
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
406
588
|
const aliasName = node.id.name;
|
|
407
589
|
const originalName = node.init.name;
|
|
590
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
408
591
|
context.report({
|
|
409
592
|
node,
|
|
410
593
|
messageId: "pointlessReassignment",
|
|
@@ -415,6 +598,7 @@ const plugin = {
|
|
|
415
598
|
fix(fixer) {
|
|
416
599
|
const variable = scope.set.get(aliasName);
|
|
417
600
|
if (!variable) return null;
|
|
601
|
+
if (aliasIsAnnotated) return null;
|
|
418
602
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
419
603
|
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
420
604
|
if (readRefs.some((reference) => {
|
|
@@ -430,10 +614,11 @@ const plugin = {
|
|
|
430
614
|
}
|
|
431
615
|
return false;
|
|
432
616
|
})) return null;
|
|
617
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
433
618
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
434
619
|
const declaration = node.parent;
|
|
435
620
|
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
436
|
-
fixes.push(fixer.remove(declaration));
|
|
621
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
437
622
|
return fixes;
|
|
438
623
|
}
|
|
439
624
|
});
|
|
@@ -465,6 +650,8 @@ const plugin = {
|
|
|
465
650
|
linterOptions: { noInlineConfig: true },
|
|
466
651
|
rules: {
|
|
467
652
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
653
|
+
"exadev/no-mutable-union-array-param": "error",
|
|
654
|
+
"exadev/no-object-assign": "error",
|
|
468
655
|
"exadev/no-pointless-reassignment": "error"
|
|
469
656
|
}
|
|
470
657
|
};
|
|
@@ -488,9 +675,13 @@ const recommendedTypeChecked = [
|
|
|
488
675
|
linterOptions: { noInlineConfig: true },
|
|
489
676
|
rules: {
|
|
490
677
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
678
|
+
"exadev/no-enum-number-widening": "error",
|
|
679
|
+
"exadev/no-mutable-union-array-param": "error",
|
|
680
|
+
"exadev/no-object-assign": "error",
|
|
491
681
|
"exadev/no-pointless-reassignment": "error",
|
|
492
682
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
493
|
-
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }]
|
|
683
|
+
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
684
|
+
"@typescript-eslint/method-signature-style": ["error", "property"]
|
|
494
685
|
}
|
|
495
686
|
},
|
|
496
687
|
{
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
2
|
//#region src/recommended-type-checked.d.ts
|
|
3
|
-
type ConfigValue = NonNullable<
|
|
3
|
+
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
4
4
|
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
5
5
|
declare const recommendedTypeChecked: ConfigArrayValue;
|
|
6
6
|
//#endregion
|
|
7
7
|
//#region src/plugin.d.ts
|
|
8
|
-
declare const plugin:
|
|
8
|
+
declare const plugin: TSESLint.FlatConfig.Plugin;
|
|
9
9
|
//#endregion
|
|
10
10
|
export { recommendedTypeChecked as default, plugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESLint } from "@typescript-eslint/utils";
|
|
2
2
|
//#region src/recommended-type-checked.d.ts
|
|
3
|
-
type ConfigValue = NonNullable<
|
|
3
|
+
type ConfigValue = NonNullable<TSESLint.FlatConfig.Plugin['configs']>[string];
|
|
4
4
|
type ConfigArrayValue = Extract<ConfigValue, unknown[]>;
|
|
5
5
|
declare const recommendedTypeChecked: ConfigArrayValue;
|
|
6
6
|
//#endregion
|
|
7
7
|
//#region src/plugin.d.ts
|
|
8
|
-
declare const plugin:
|
|
8
|
+
declare const plugin: TSESLint.FlatConfig.Plugin;
|
|
9
9
|
//#endregion
|
|
10
10
|
export { recommendedTypeChecked as default, plugin };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import tseslint from "typescript-eslint";
|
|
2
2
|
import { posix } from "node:path";
|
|
3
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
4
|
+
import * as ts from "typescript";
|
|
3
5
|
//#region package.json
|
|
4
|
-
var version = "2.
|
|
6
|
+
var version = "2.2.0";
|
|
5
7
|
//#endregion
|
|
6
8
|
//#region src/rules/barrel-helpers.ts
|
|
7
9
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -237,6 +239,51 @@ const barrelPolicy = {
|
|
|
237
239
|
}
|
|
238
240
|
};
|
|
239
241
|
//#endregion
|
|
242
|
+
//#region src/rules/no-enum-number-widening.ts
|
|
243
|
+
const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
244
|
+
name: "no-enum-number-widening",
|
|
245
|
+
meta: {
|
|
246
|
+
type: "problem",
|
|
247
|
+
docs: { description: "Disallow assigning a bare (non-literal) number where a numeric enum type is expected -- TypeScript accepts any number for a numeric enum slot, not just its own members, once the value is not a literal the compiler can range-check." },
|
|
248
|
+
schema: [],
|
|
249
|
+
messages: { widening: "A plain 'number' value is being used where the numeric enum '{{ enumName }}' is expected. TypeScript does not verify the value is actually one of the enum's members here -- narrow it to a known member first (e.g. a lookup/guard against the enum's own values), or accept a plain 'number' parameter instead of widening it implicitly." }
|
|
250
|
+
},
|
|
251
|
+
defaultOptions: [],
|
|
252
|
+
create(context) {
|
|
253
|
+
const services = ESLintUtils.getParserServices(context);
|
|
254
|
+
const checker = services.program.getTypeChecker();
|
|
255
|
+
function checkNode(expression) {
|
|
256
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(expression);
|
|
257
|
+
if (!ts.isExpression(tsNode)) return;
|
|
258
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
259
|
+
if (!contextualType || !(contextualType.flags & ts.TypeFlags.EnumLike)) return;
|
|
260
|
+
const actualType = checker.getTypeAtLocation(tsNode);
|
|
261
|
+
if (actualType.flags & ts.TypeFlags.EnumLike) return;
|
|
262
|
+
if (actualType.isLiteral()) return;
|
|
263
|
+
if (!(actualType.flags & ts.TypeFlags.NumberLike)) return;
|
|
264
|
+
context.report({
|
|
265
|
+
node: expression,
|
|
266
|
+
messageId: "widening",
|
|
267
|
+
data: { enumName: checker.typeToString(contextualType) }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
VariableDeclarator(node) {
|
|
272
|
+
if (node.init) checkNode(node.init);
|
|
273
|
+
},
|
|
274
|
+
AssignmentExpression(node) {
|
|
275
|
+
checkNode(node.right);
|
|
276
|
+
},
|
|
277
|
+
ReturnStatement(node) {
|
|
278
|
+
if (node.argument) checkNode(node.argument);
|
|
279
|
+
},
|
|
280
|
+
CallExpression(node) {
|
|
281
|
+
for (const argument of node.arguments) if (argument.type !== AST_NODE_TYPES.SpreadElement) checkNode(argument);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
//#endregion
|
|
240
287
|
//#region src/rules/no-index-files.ts
|
|
241
288
|
const noIndexFiles = {
|
|
242
289
|
meta: {
|
|
@@ -255,6 +302,58 @@ const noIndexFiles = {
|
|
|
255
302
|
}
|
|
256
303
|
};
|
|
257
304
|
//#endregion
|
|
305
|
+
//#region src/rules/no-mutable-union-array-param.ts
|
|
306
|
+
const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
307
|
+
"push",
|
|
308
|
+
"unshift",
|
|
309
|
+
"splice",
|
|
310
|
+
"fill",
|
|
311
|
+
"copyWithin"
|
|
312
|
+
]);
|
|
313
|
+
const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
314
|
+
function isUnionArrayType(typeAnnotation) {
|
|
315
|
+
if (typeAnnotation.type === AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
316
|
+
if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "Array" && typeAnnotation.typeArguments?.params.length === 1) {
|
|
317
|
+
const firstParam = typeAnnotation.typeArguments.params[0];
|
|
318
|
+
if (firstParam?.type === AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const noMutableUnionArrayParam = createRule$1({
|
|
322
|
+
name: "no-mutable-union-array-param",
|
|
323
|
+
meta: {
|
|
324
|
+
type: "problem",
|
|
325
|
+
fixable: "code",
|
|
326
|
+
docs: { description: "Disallow mutating-insertion calls on a union-element array parameter, which lets a caller pass a narrower array whose declared element type the call can silently violate." },
|
|
327
|
+
schema: [],
|
|
328
|
+
messages: { unsound: "'{{ method }}' inserts into a parameter typed as an array of a union -- a caller may have passed a narrower array (e.g. number[] where (string | number)[] is declared), and TypeScript's covariant array typing does not catch the resulting mismatch. Mark the parameter readonly to turn this into a real compile error, or narrow the parameter type." }
|
|
329
|
+
},
|
|
330
|
+
defaultOptions: [],
|
|
331
|
+
create(context) {
|
|
332
|
+
return { CallExpression(node) {
|
|
333
|
+
const { callee } = node;
|
|
334
|
+
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.has(callee.property.name)) return;
|
|
335
|
+
const parameterDefinition = (context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved)?.defs.find((definition) => definition.type === TSESLint.Scope.DefinitionType.Parameter);
|
|
336
|
+
if (!parameterDefinition) return;
|
|
337
|
+
const parameterNode = parameterDefinition.name;
|
|
338
|
+
if (parameterNode.type !== AST_NODE_TYPES.Identifier || !parameterNode.typeAnnotation) return;
|
|
339
|
+
if (!isUnionArrayType(parameterNode.typeAnnotation.typeAnnotation)) return;
|
|
340
|
+
context.report({
|
|
341
|
+
node,
|
|
342
|
+
messageId: "unsound",
|
|
343
|
+
data: { method: callee.property.name },
|
|
344
|
+
fix(fixer) {
|
|
345
|
+
const typeAnnotation = parameterNode.typeAnnotation;
|
|
346
|
+
if (!typeAnnotation) return null;
|
|
347
|
+
const annotated = typeAnnotation.typeAnnotation;
|
|
348
|
+
if (annotated.type === AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
349
|
+
if (annotated.type === AST_NODE_TYPES.TSTypeReference && annotated.typeName.type === AST_NODE_TYPES.Identifier) return fixer.replaceText(annotated.typeName, "ReadonlyArray");
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
} };
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
//#endregion
|
|
258
357
|
//#region src/rules/no-non-barrel-index.ts
|
|
259
358
|
const INDEX_BASENAME = /^index\.[cm]?[tj]s$/;
|
|
260
359
|
const noNonBarrelIndex = {
|
|
@@ -344,10 +443,89 @@ const noNonBarrelReexport = {
|
|
|
344
443
|
}
|
|
345
444
|
};
|
|
346
445
|
//#endregion
|
|
446
|
+
//#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`);
|
|
448
|
+
function resolveFrom$1(scope, name) {
|
|
449
|
+
for (let current = scope; current; current = current.upper) {
|
|
450
|
+
const found = current.set.get(name);
|
|
451
|
+
if (found) return found;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const noObjectAssign = createRule({
|
|
455
|
+
name: "no-object-assign",
|
|
456
|
+
meta: {
|
|
457
|
+
type: "problem",
|
|
458
|
+
fixable: "code",
|
|
459
|
+
hasSuggestions: true,
|
|
460
|
+
docs: { description: "Disallow Object.assign, whose own type declarations do not check a source object's properties against the target's declared types -- object spread does." },
|
|
461
|
+
schema: [],
|
|
462
|
+
messages: {
|
|
463
|
+
unsound: "Object.assign does not verify that a source object's properties are assignable to the target's declared types, so a type mismatch here passes silently where a direct property assignment would be rejected. Use object spread ({ ...target, ...source }) to build a correctly type-checked replacement instead.",
|
|
464
|
+
suggestSpreadReassign: "Replace with object spread and reassignment (changes the object reference -- anything else already holding this object will not see the update)."
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
defaultOptions: [],
|
|
468
|
+
create(context) {
|
|
469
|
+
return { CallExpression(node) {
|
|
470
|
+
const { callee } = node;
|
|
471
|
+
if (!(callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES.Identifier && callee.object.name === "Object" && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "assign")) return;
|
|
472
|
+
const [target, ...sources] = node.arguments;
|
|
473
|
+
const sourcesAreSpreadable = sources.every((argument) => argument.type !== AST_NODE_TYPES.SpreadElement);
|
|
474
|
+
if (target?.type === AST_NODE_TYPES.ObjectExpression && sourcesAreSpreadable) {
|
|
475
|
+
const isBareStatement = node.parent.type === AST_NODE_TYPES.ExpressionStatement && node.parent.expression === node;
|
|
476
|
+
context.report({
|
|
477
|
+
node,
|
|
478
|
+
messageId: "unsound",
|
|
479
|
+
fix(fixer) {
|
|
480
|
+
const targetProps = target.properties.map((property) => context.sourceCode.getText(property));
|
|
481
|
+
const sourceSpreads = sources.map((argument) => `...${context.sourceCode.getText(argument)}`);
|
|
482
|
+
const objectLiteral = `{ ${[...targetProps, ...sourceSpreads].join(", ")} }`;
|
|
483
|
+
return fixer.replaceText(node, isBareStatement ? `(${objectLiteral})` : objectLiteral);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (target?.type === AST_NODE_TYPES.Identifier && sourcesAreSpreadable && node.parent.type === AST_NODE_TYPES.ExpressionStatement && node.parent.expression === node) {
|
|
489
|
+
const targetName = target.name;
|
|
490
|
+
const variable = resolveFrom$1(context.sourceCode.getScope(node), targetName);
|
|
491
|
+
const isConst = (variable?.defs.find((candidate) => candidate.type === TSESLint.Scope.DefinitionType.Variable))?.parent.kind === "const";
|
|
492
|
+
const statement = node.parent;
|
|
493
|
+
if (variable && !isConst) {
|
|
494
|
+
context.report({
|
|
495
|
+
node,
|
|
496
|
+
messageId: "unsound",
|
|
497
|
+
suggest: [{
|
|
498
|
+
messageId: "suggestSpreadReassign",
|
|
499
|
+
fix(fixer) {
|
|
500
|
+
const sourceSpreads = sources.map((argument) => `...${context.sourceCode.getText(argument)}`);
|
|
501
|
+
return fixer.replaceText(statement, `${targetName} = { ...${targetName}, ${sourceSpreads.join(", ")} };`);
|
|
502
|
+
}
|
|
503
|
+
}]
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
context.report({
|
|
509
|
+
node,
|
|
510
|
+
messageId: "unsound"
|
|
511
|
+
});
|
|
512
|
+
} };
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
//#endregion
|
|
347
516
|
//#region src/rules/no-pointless-reassignment.ts
|
|
348
517
|
function isIdentifierReference(reference) {
|
|
349
518
|
return reference.identifier.type === "Identifier";
|
|
350
519
|
}
|
|
520
|
+
function hasTypeAnnotation(id) {
|
|
521
|
+
return "typeAnnotation" in id && id.typeAnnotation !== void 0 && id.typeAnnotation !== null;
|
|
522
|
+
}
|
|
523
|
+
function resolveFrom(scope, name) {
|
|
524
|
+
for (let current = scope; current; current = current.upper) {
|
|
525
|
+
const found = current.set.get(name);
|
|
526
|
+
if (found) return found;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
351
529
|
//#endregion
|
|
352
530
|
//#region src/plugin.ts
|
|
353
531
|
const plugin = {
|
|
@@ -359,9 +537,12 @@ const plugin = {
|
|
|
359
537
|
rules: {
|
|
360
538
|
"barrel-direct-siblings-only": barrelDirectSiblingsOnly,
|
|
361
539
|
"barrel-policy": barrelPolicy,
|
|
540
|
+
"no-enum-number-widening": noEnumNumberWidening,
|
|
362
541
|
"no-index-files": noIndexFiles,
|
|
542
|
+
"no-mutable-union-array-param": noMutableUnionArrayParam,
|
|
363
543
|
"no-non-barrel-index": noNonBarrelIndex,
|
|
364
544
|
"no-non-barrel-reexport": noNonBarrelReexport,
|
|
545
|
+
"no-object-assign": noObjectAssign,
|
|
365
546
|
"no-pointless-reassignment": {
|
|
366
547
|
meta: {
|
|
367
548
|
type: "problem",
|
|
@@ -378,6 +559,7 @@ const plugin = {
|
|
|
378
559
|
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
379
560
|
const aliasName = node.id.name;
|
|
380
561
|
const originalName = node.init.name;
|
|
562
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
381
563
|
context.report({
|
|
382
564
|
node,
|
|
383
565
|
messageId: "pointlessReassignment",
|
|
@@ -388,6 +570,7 @@ const plugin = {
|
|
|
388
570
|
fix(fixer) {
|
|
389
571
|
const variable = scope.set.get(aliasName);
|
|
390
572
|
if (!variable) return null;
|
|
573
|
+
if (aliasIsAnnotated) return null;
|
|
391
574
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
392
575
|
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
393
576
|
if (readRefs.some((reference) => {
|
|
@@ -403,10 +586,11 @@ const plugin = {
|
|
|
403
586
|
}
|
|
404
587
|
return false;
|
|
405
588
|
})) return null;
|
|
589
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
406
590
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
407
591
|
const declaration = node.parent;
|
|
408
592
|
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
409
|
-
fixes.push(fixer.remove(declaration));
|
|
593
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
410
594
|
return fixes;
|
|
411
595
|
}
|
|
412
596
|
});
|
|
@@ -438,6 +622,8 @@ const plugin = {
|
|
|
438
622
|
linterOptions: { noInlineConfig: true },
|
|
439
623
|
rules: {
|
|
440
624
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
625
|
+
"exadev/no-mutable-union-array-param": "error",
|
|
626
|
+
"exadev/no-object-assign": "error",
|
|
441
627
|
"exadev/no-pointless-reassignment": "error"
|
|
442
628
|
}
|
|
443
629
|
};
|
|
@@ -461,9 +647,13 @@ const recommendedTypeChecked = [
|
|
|
461
647
|
linterOptions: { noInlineConfig: true },
|
|
462
648
|
rules: {
|
|
463
649
|
"exadev/barrel-policy": ["error", { mode: "banned" }],
|
|
650
|
+
"exadev/no-enum-number-widening": "error",
|
|
651
|
+
"exadev/no-mutable-union-array-param": "error",
|
|
652
|
+
"exadev/no-object-assign": "error",
|
|
464
653
|
"exadev/no-pointless-reassignment": "error",
|
|
465
654
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
|
|
466
|
-
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }]
|
|
655
|
+
"@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
|
|
656
|
+
"@typescript-eslint/method-signature-style": ["error", "property"]
|
|
467
657
|
}
|
|
468
658
|
},
|
|
469
659
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exadev-eslint-config",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Shared custom ESLint rules and plugin for ExaDev projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"eslint": ">=10.0.0",
|
|
41
|
+
"typescript": ">=4.8.4",
|
|
41
42
|
"typescript-eslint": ">=8.0.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@semantic-release/changelog": "^7.0.0",
|
|
49
50
|
"@semantic-release/git": "^11.0.1",
|
|
50
51
|
"@types/node": "^24.9.2",
|
|
52
|
+
"@typescript-eslint/rule-tester": "^8.67.0",
|
|
51
53
|
"@vitest/coverage-v8": "^4.1.10",
|
|
52
54
|
"eslint": "^10.8.0",
|
|
53
55
|
"husky": "^9.1.7",
|
|
@@ -57,9 +59,12 @@
|
|
|
57
59
|
"tsdown": "^0.22.13",
|
|
58
60
|
"turbo": "^2.10.8",
|
|
59
61
|
"typescript": "^6.0.3",
|
|
60
|
-
"typescript-eslint": "^8.
|
|
62
|
+
"typescript-eslint": "^8.67.0",
|
|
61
63
|
"vitest": "^4.1.10"
|
|
62
64
|
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@typescript-eslint/utils": "^8.67.0"
|
|
67
|
+
},
|
|
63
68
|
"scripts": {
|
|
64
69
|
"build": "turbo run _build",
|
|
65
70
|
"_build": "tsdown",
|