exadev-eslint-config 2.17.0 → 2.17.2
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/dist/index.cjs +196 -213
- package/dist/index.js +196 -213
- package/package.json +8 -2
package/dist/index.cjs
CHANGED
|
@@ -263,9 +263,6 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
263
263
|
function isRecord$1(value) {
|
|
264
264
|
return typeof value === "object" && value !== null;
|
|
265
265
|
}
|
|
266
|
-
function isFlatConfig$1(value) {
|
|
267
|
-
return isRecord$1(value);
|
|
268
|
-
}
|
|
269
266
|
function normalizeLegacyParserOptions(record) {
|
|
270
267
|
if (!("parserOptions" in record)) return record;
|
|
271
268
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
@@ -285,8 +282,7 @@ function readFlatConfig(module, path) {
|
|
|
285
282
|
current = current[key];
|
|
286
283
|
}
|
|
287
284
|
if (!isRecord$1(current)) return void 0;
|
|
288
|
-
|
|
289
|
-
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
285
|
+
return normalizeLegacyParserOptions(current);
|
|
290
286
|
}
|
|
291
287
|
//#endregion
|
|
292
288
|
//#region src/nextjs.ts
|
|
@@ -299,7 +295,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
299
295
|
}
|
|
300
296
|
//#endregion
|
|
301
297
|
//#region package.json
|
|
302
|
-
var version = "2.17.
|
|
298
|
+
var version = "2.17.2";
|
|
303
299
|
//#endregion
|
|
304
300
|
//#region src/react.ts
|
|
305
301
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -341,12 +337,15 @@ function buildReactConfig(options = {}) {
|
|
|
341
337
|
//#region src/rules/barrel-helpers.ts
|
|
342
338
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
343
339
|
function basenameOf(filename) {
|
|
344
|
-
|
|
345
|
-
return slash === -1 ? filename : filename.slice(slash + 1);
|
|
340
|
+
return filename.slice(filename.lastIndexOf("/") + 1);
|
|
346
341
|
}
|
|
347
342
|
function isIndexFile(filename) {
|
|
348
343
|
return INDEX_BASENAME$1.test(basenameOf(filename));
|
|
349
344
|
}
|
|
345
|
+
function moduleSpecifierValue(literal) {
|
|
346
|
+
if (typeof literal.value !== "string") throw new Error(`Unreachable: a module specifier's own grammar only ever produces a string literal, got ${typeof literal.value} instead.`);
|
|
347
|
+
return literal.value;
|
|
348
|
+
}
|
|
350
349
|
function isMainBarrel(filename) {
|
|
351
350
|
return filename.endsWith("/src/index.ts");
|
|
352
351
|
}
|
|
@@ -381,6 +380,9 @@ function isInsideAmbientModuleDeclaration(node) {
|
|
|
381
380
|
}
|
|
382
381
|
return false;
|
|
383
382
|
}
|
|
383
|
+
function hasSource(node) {
|
|
384
|
+
return node.source !== null && node.source !== void 0;
|
|
385
|
+
}
|
|
384
386
|
function createSplitReexportDetector() {
|
|
385
387
|
const importsByName = /* @__PURE__ */ new Map();
|
|
386
388
|
const bareExportSpecifiers = [];
|
|
@@ -418,14 +420,15 @@ function createSplitReexportDetector() {
|
|
|
418
420
|
});
|
|
419
421
|
}
|
|
420
422
|
for (const declarationNode of defaultExportDeclarations) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const trackedImport = importsByName.get(name);
|
|
423
|
+
if (declarationNode.declaration.type !== "Identifier") continue;
|
|
424
|
+
const identifierNode = declarationNode.declaration;
|
|
425
|
+
const trackedImport = importsByName.get(identifierNode.name);
|
|
424
426
|
if (trackedImport === void 0) continue;
|
|
425
427
|
out.push({
|
|
426
428
|
kind: "default",
|
|
427
429
|
declaration: declarationNode,
|
|
428
|
-
|
|
430
|
+
identifierNode,
|
|
431
|
+
name: identifierNode.name,
|
|
429
432
|
trackedImport
|
|
430
433
|
});
|
|
431
434
|
}
|
|
@@ -439,15 +442,14 @@ const barrelDirectSiblingsOnly = {
|
|
|
439
442
|
meta: {
|
|
440
443
|
type: "problem",
|
|
441
444
|
schema: [],
|
|
442
|
-
messages: { notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts')
|
|
445
|
+
messages: { notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts') — found '{{ source }}'. Move the source closer, or import it directly at the call site rather than re-exporting it through this barrel." }
|
|
443
446
|
},
|
|
444
447
|
create(context) {
|
|
445
448
|
if (!isIndexFile(context.filename)) return {};
|
|
446
449
|
return {
|
|
447
450
|
ExportNamedDeclaration(node) {
|
|
448
451
|
if (node.source === null || node.source === void 0) return;
|
|
449
|
-
const source = node.source
|
|
450
|
-
if (typeof source !== "string") return;
|
|
452
|
+
const source = moduleSpecifierValue(node.source);
|
|
451
453
|
if (!isDirectSibling(source)) context.report({
|
|
452
454
|
node,
|
|
453
455
|
messageId: "notADirectSibling",
|
|
@@ -455,8 +457,7 @@ const barrelDirectSiblingsOnly = {
|
|
|
455
457
|
});
|
|
456
458
|
},
|
|
457
459
|
ExportAllDeclaration(node) {
|
|
458
|
-
const source = node.source
|
|
459
|
-
if (typeof source !== "string") return;
|
|
460
|
+
const source = moduleSpecifierValue(node.source);
|
|
460
461
|
if (!isDirectSibling(source)) context.report({
|
|
461
462
|
node,
|
|
462
463
|
messageId: "notADirectSibling",
|
|
@@ -469,7 +470,7 @@ const barrelDirectSiblingsOnly = {
|
|
|
469
470
|
//#endregion
|
|
470
471
|
//#region src/rules/barrel-policy.ts
|
|
471
472
|
function readMode(options) {
|
|
472
|
-
if (
|
|
473
|
+
if (typeof options !== "object" || options === null || !("mode" in options) || !isBarrelMode(options.mode)) throw new Error("exadev/barrel-policy requires options: { mode: 'banned' | 'single' | 'siblings' }.");
|
|
473
474
|
return options.mode;
|
|
474
475
|
}
|
|
475
476
|
const barrelPolicy = {
|
|
@@ -489,20 +490,17 @@ const barrelPolicy = {
|
|
|
489
490
|
additionalProperties: false
|
|
490
491
|
}],
|
|
491
492
|
messages: {
|
|
492
|
-
indexFileBanned: "Index (barrel) files are banned in this project
|
|
493
|
-
nonMainIndexFile: "Only src/index.ts may be a barrel in this project
|
|
494
|
-
sideEffectInBarrel: "A barrel may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...')
|
|
495
|
-
reexportOutsideBarrel: "Re-exports belong only in a barrel (index) file
|
|
496
|
-
notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts')
|
|
493
|
+
indexFileBanned: "Index (barrel) files are banned in this project — import directly from the module that owns the export instead. Rename this file to something descriptive.",
|
|
494
|
+
nonMainIndexFile: "Only src/index.ts may be a barrel in this project — this index file is not it. Move its contents into the module that owns them or give the file a descriptive name.",
|
|
495
|
+
sideEffectInBarrel: "A barrel 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 }}.",
|
|
496
|
+
reexportOutsideBarrel: "Re-exports belong only in a barrel (index) file — import this value directly in the file that uses it instead of re-exporting it through this one.",
|
|
497
|
+
notADirectSibling: "A barrel may re-export only from a direct sibling file or folder ('./module' or './module.ts') — found '{{ source }}'. Move the source closer, or import it directly at the call site rather than re-exporting it through this barrel."
|
|
497
498
|
}
|
|
498
499
|
},
|
|
499
500
|
create(context) {
|
|
500
501
|
const mode = readMode(context.options[0]);
|
|
501
502
|
const filename = context.filename;
|
|
502
503
|
const detector = createSplitReexportDetector();
|
|
503
|
-
function hasSource(node) {
|
|
504
|
-
return node.source !== null && node.source !== void 0;
|
|
505
|
-
}
|
|
506
504
|
return {
|
|
507
505
|
Program(node) {
|
|
508
506
|
if (mode === "banned") {
|
|
@@ -543,12 +541,12 @@ const barrelPolicy = {
|
|
|
543
541
|
ExportNamedDeclaration(node) {
|
|
544
542
|
detector.visitExportNamed(node);
|
|
545
543
|
if (hasSource(node) && !isInsideAmbientModuleDeclaration(node)) {
|
|
546
|
-
const source = node.source
|
|
544
|
+
const source = moduleSpecifierValue(node.source);
|
|
547
545
|
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
548
546
|
node,
|
|
549
547
|
messageId: "reexportOutsideBarrel"
|
|
550
548
|
});
|
|
551
|
-
else if (mode === "siblings" &&
|
|
549
|
+
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
552
550
|
node,
|
|
553
551
|
messageId: "notADirectSibling",
|
|
554
552
|
data: { source }
|
|
@@ -557,12 +555,12 @@ const barrelPolicy = {
|
|
|
557
555
|
},
|
|
558
556
|
ExportAllDeclaration(node) {
|
|
559
557
|
if (isInsideAmbientModuleDeclaration(node)) return;
|
|
560
|
-
const source = node.source
|
|
558
|
+
const source = moduleSpecifierValue(node.source);
|
|
561
559
|
if (!isPermittedBarrel(filename, mode)) context.report({
|
|
562
560
|
node,
|
|
563
561
|
messageId: "reexportOutsideBarrel"
|
|
564
562
|
});
|
|
565
|
-
else if (mode === "siblings" &&
|
|
563
|
+
else if (mode === "siblings" && !isDirectSibling(source)) context.report({
|
|
566
564
|
node,
|
|
567
565
|
messageId: "notADirectSibling",
|
|
568
566
|
data: { source }
|
|
@@ -574,8 +572,8 @@ const barrelPolicy = {
|
|
|
574
572
|
"Program:exit"() {
|
|
575
573
|
for (const violation of detector.violations()) if (isPermittedBarrel(filename, mode)) {
|
|
576
574
|
if (mode === "siblings") {
|
|
577
|
-
const importSource = violation.trackedImport.declaration.source
|
|
578
|
-
if (
|
|
575
|
+
const importSource = moduleSpecifierValue(violation.trackedImport.declaration.source);
|
|
576
|
+
if (!isDirectSibling(importSource)) context.report({
|
|
579
577
|
node: violation.kind === "named" ? violation.specifier : violation.declaration,
|
|
580
578
|
messageId: "notADirectSibling",
|
|
581
579
|
data: { source: importSource }
|
|
@@ -590,6 +588,12 @@ const barrelPolicy = {
|
|
|
590
588
|
}
|
|
591
589
|
};
|
|
592
590
|
//#endregion
|
|
591
|
+
//#region src/rules/scope-guards.ts
|
|
592
|
+
function asIdentifierName(name) {
|
|
593
|
+
if (name.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) throw new Error(`Unreachable: expected a Parameter/Variable Definition's own name to be an Identifier, got ${name.type} instead.`);
|
|
594
|
+
return name;
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
593
597
|
//#region src/rules/no-array-isarray-mutation.ts
|
|
594
598
|
const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
595
599
|
"push",
|
|
@@ -616,7 +620,7 @@ const noArrayIsArrayMutation = createRule$7({
|
|
|
616
620
|
type: "problem",
|
|
617
621
|
schema: [],
|
|
618
622
|
docs: { description: "Disallow mutating-insertion calls on a parameter or local variable whose real type includes a readonly array, narrowed via Array.isArray, which silently discards the declared readonly guarantee." },
|
|
619
|
-
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by Array.isArray
|
|
623
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by Array.isArray — Array.isArray's own type declaration cannot preserve a readonly modifier through the guard, so a value whose real type includes a readonly array (a caller's array, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the array before inserting (e.g. a spread into a new array), or narrow with a check that preserves readonly instead of Array.isArray." }
|
|
620
624
|
},
|
|
621
625
|
defaultOptions: [],
|
|
622
626
|
create(context) {
|
|
@@ -629,14 +633,12 @@ const noArrayIsArrayMutation = createRule$7({
|
|
|
629
633
|
}
|
|
630
634
|
return { CallExpression(node) {
|
|
631
635
|
const { callee } = node;
|
|
632
|
-
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.
|
|
636
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS$1.has(callee.property.name)) return;
|
|
633
637
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
634
638
|
if (!variable) return;
|
|
635
639
|
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
636
640
|
if (!declarationDefinition) return;
|
|
637
|
-
|
|
638
|
-
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
639
|
-
if (!declarationHasReadonlyArrayConstituent(declarationNode)) return;
|
|
641
|
+
if (!declarationHasReadonlyArrayConstituent(asIdentifierName(declarationDefinition.name))) return;
|
|
640
642
|
if (!isGuardedByArrayIsArray(node, variable, context)) return;
|
|
641
643
|
context.report({
|
|
642
644
|
node,
|
|
@@ -664,7 +666,7 @@ const noArrayIsArrayMutation = createRule$7({
|
|
|
664
666
|
if (parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
665
667
|
if (parent.alternate === current && isNegatedArrayIsArrayCall(parent.test, parameterVariable, ruleContext)) return true;
|
|
666
668
|
}
|
|
667
|
-
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" &&
|
|
669
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && matchesArrayIsArrayOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
668
670
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesArrayIsArrayOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
669
671
|
current = parent;
|
|
670
672
|
}
|
|
@@ -676,15 +678,7 @@ const noArrayIsArrayMutation = createRule$7({
|
|
|
676
678
|
const { parent } = current;
|
|
677
679
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
678
680
|
const statements = parent.body;
|
|
679
|
-
|
|
680
|
-
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
681
|
-
ownIndex = i;
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
685
|
-
const sibling = statements[i];
|
|
686
|
-
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent)) return true;
|
|
687
|
-
}
|
|
681
|
+
if (statements.slice(0, statements.indexOf(current)).some((sibling) => sibling.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedArrayIsArrayCall(sibling.test, parameterVariable, ruleContext) && definitelyExits$2(sibling.consequent))) return true;
|
|
688
682
|
}
|
|
689
683
|
current = parent;
|
|
690
684
|
}
|
|
@@ -763,22 +757,36 @@ const noControlFlow = {
|
|
|
763
757
|
}
|
|
764
758
|
};
|
|
765
759
|
//#endregion
|
|
760
|
+
//#region src/rules/ts-node-guards.ts
|
|
761
|
+
function asExpression(tsNode) {
|
|
762
|
+
if (!typescript.isExpression(tsNode)) throw new Error(`Unreachable: expected a ts.Expression, got ts.SyntaxKind.${typescript.SyntaxKind[tsNode.kind]} instead.`);
|
|
763
|
+
return tsNode;
|
|
764
|
+
}
|
|
765
|
+
function asTypeReference(type) {
|
|
766
|
+
if (!(0, ts_api_utils.isTypeReference)(type)) throw new Error("Unreachable: expected an array/tuple type to be backed by a ts.TypeReference.");
|
|
767
|
+
return type;
|
|
768
|
+
}
|
|
769
|
+
function lastTokenOrThrow(sourceCode, node) {
|
|
770
|
+
const token = sourceCode.getLastToken(node);
|
|
771
|
+
if (token === null) throw new Error("Unreachable: getLastToken returned null for a node expected to always have at least one token.");
|
|
772
|
+
return token;
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
766
775
|
//#region src/rules/no-enum-number-widening.ts
|
|
767
776
|
const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
768
777
|
name: "no-enum-number-widening",
|
|
769
778
|
meta: {
|
|
770
779
|
type: "problem",
|
|
771
|
-
docs: { description: "Disallow assigning a bare (non-literal) number where a numeric enum type is expected
|
|
780
|
+
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." },
|
|
772
781
|
schema: [],
|
|
773
|
-
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
|
|
782
|
+
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." },
|
|
783
|
+
defaultOptions: []
|
|
774
784
|
},
|
|
775
|
-
defaultOptions: [],
|
|
776
785
|
create(context) {
|
|
777
786
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
778
787
|
const checker = services.program.getTypeChecker();
|
|
779
788
|
function checkNode(expression) {
|
|
780
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(expression);
|
|
781
|
-
if (!typescript.isExpression(tsNode)) return;
|
|
789
|
+
const tsNode = asExpression(services.esTreeNodeToTSNodeMap.get(expression));
|
|
782
790
|
const contextualType = checker.getContextualType(tsNode);
|
|
783
791
|
if (!contextualType || !(contextualType.flags & typescript.TypeFlags.EnumLike)) return;
|
|
784
792
|
const actualType = checker.getTypeAtLocation(tsNode);
|
|
@@ -814,27 +822,24 @@ const noEnumReverseLookupWidening = _typescript_eslint_utils.ESLintUtils.RuleCre
|
|
|
814
822
|
meta: {
|
|
815
823
|
type: "problem",
|
|
816
824
|
hasSuggestions: true,
|
|
817
|
-
docs: { description: "Disallow indexing a numeric enum's reverse mapping with a bare (non-literal) number
|
|
825
|
+
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." },
|
|
818
826
|
schema: [],
|
|
819
827
|
messages: {
|
|
820
|
-
widening: "Indexing the numeric enum '{{ enumName }}' with a plain 'number' relies on its reverse mapping, which TypeScript types as 'string' for any number
|
|
828
|
+
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.",
|
|
821
829
|
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."
|
|
822
|
-
}
|
|
830
|
+
},
|
|
831
|
+
defaultOptions: []
|
|
823
832
|
},
|
|
824
|
-
defaultOptions: [],
|
|
825
833
|
create(context) {
|
|
826
834
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
827
835
|
const checker = services.program.getTypeChecker();
|
|
828
836
|
return { MemberExpression(node) {
|
|
829
|
-
|
|
830
|
-
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
|
|
831
|
-
if (!typescript.isExpression(objectTsNode)) return;
|
|
837
|
+
const objectTsNode = asExpression(services.esTreeNodeToTSNodeMap.get(node.object));
|
|
832
838
|
const objectType = checker.getTypeAtLocation(objectTsNode);
|
|
833
839
|
const objectSymbol = objectType.getSymbol();
|
|
834
840
|
if (!objectSymbol || !(objectSymbol.flags & typescript.SymbolFlags.Enum)) return;
|
|
835
841
|
if (!checker.getIndexInfoOfType(objectType, typescript.IndexKind.Number)) return;
|
|
836
|
-
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(node.property);
|
|
837
|
-
if (!typescript.isExpression(propertyTsNode)) return;
|
|
842
|
+
const propertyTsNode = asExpression(services.esTreeNodeToTSNodeMap.get(node.property));
|
|
838
843
|
const rawPropertyType = checker.getTypeAtLocation(propertyTsNode);
|
|
839
844
|
const propertyType = checker.getBaseConstraintOfType(rawPropertyType) ?? rawPropertyType;
|
|
840
845
|
if (propertyType.flags & typescript.TypeFlags.EnumLike) {
|
|
@@ -845,7 +850,7 @@ const noEnumReverseLookupWidening = _typescript_eslint_utils.ESLintUtils.RuleCre
|
|
|
845
850
|
}
|
|
846
851
|
const enumName = checker.typeToString(checker.getDeclaredTypeOfSymbol(objectSymbol));
|
|
847
852
|
const parent = node.parent;
|
|
848
|
-
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator && parent.
|
|
853
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator && parent.id.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && parent.id.typeAnnotation?.typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSStringKeyword) {
|
|
849
854
|
const stringKeyword = parent.id.typeAnnotation.typeAnnotation;
|
|
850
855
|
context.report({
|
|
851
856
|
node,
|
|
@@ -874,7 +879,7 @@ const noIndexFiles = {
|
|
|
874
879
|
meta: {
|
|
875
880
|
type: "problem",
|
|
876
881
|
schema: [],
|
|
877
|
-
messages: { indexFileBanned: "Index (barrel) files are banned
|
|
882
|
+
messages: { indexFileBanned: "Index (barrel) files are banned — import directly from the module that owns the export instead. Rename this file to something descriptive." }
|
|
878
883
|
},
|
|
879
884
|
create(context) {
|
|
880
885
|
if (!isIndexFile(context.filename)) return {};
|
|
@@ -911,7 +916,7 @@ const noMapInstanceofMutation = createRule$6({
|
|
|
911
916
|
type: "problem",
|
|
912
917
|
schema: [],
|
|
913
918
|
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlyMap, narrowed via `instanceof Map`, which silently discards the declared readonly guarantee." },
|
|
914
|
-
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by 'instanceof Map'
|
|
919
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by 'instanceof Map' — Map is declared as extending ReadonlyMap, so 'instanceof Map' narrows straight past the readonly guarantee to the full mutable interface, and a value whose real type includes ReadonlyMap (a caller's map, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the map before mutating (e.g. `new Map(input)`), or narrow with a check that preserves readonly instead of 'instanceof Map'." }
|
|
915
920
|
},
|
|
916
921
|
defaultOptions: [],
|
|
917
922
|
create(context) {
|
|
@@ -924,14 +929,12 @@ const noMapInstanceofMutation = createRule$6({
|
|
|
924
929
|
}
|
|
925
930
|
return { CallExpression(node) {
|
|
926
931
|
const { callee } = node;
|
|
927
|
-
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.
|
|
932
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_MAP_METHODS.has(callee.property.name)) return;
|
|
928
933
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
929
934
|
if (!variable) return;
|
|
930
935
|
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
931
936
|
if (!declarationDefinition) return;
|
|
932
|
-
|
|
933
|
-
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
934
|
-
if (!declarationHasReadonlyMapConstituent(declarationNode)) return;
|
|
937
|
+
if (!declarationHasReadonlyMapConstituent(asIdentifierName(declarationDefinition.name))) return;
|
|
935
938
|
if (!isGuardedByInstanceofMap(node, variable, context)) return;
|
|
936
939
|
context.report({
|
|
937
940
|
node,
|
|
@@ -959,7 +962,7 @@ const noMapInstanceofMutation = createRule$6({
|
|
|
959
962
|
if (parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
960
963
|
if (parent.alternate === current && isNegatedInstanceofMapExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
961
964
|
}
|
|
962
|
-
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" &&
|
|
965
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && matchesInstanceofMapOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
963
966
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesInstanceofMapOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
964
967
|
current = parent;
|
|
965
968
|
}
|
|
@@ -971,15 +974,7 @@ const noMapInstanceofMutation = createRule$6({
|
|
|
971
974
|
const { parent } = current;
|
|
972
975
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
973
976
|
const statements = parent.body;
|
|
974
|
-
|
|
975
|
-
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
976
|
-
ownIndex = i;
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
979
|
-
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
980
|
-
const sibling = statements[i];
|
|
981
|
-
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedInstanceofMapExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits$1(sibling.consequent)) return true;
|
|
982
|
-
}
|
|
977
|
+
if (statements.slice(0, statements.indexOf(current)).some((sibling) => sibling.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedInstanceofMapExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits$1(sibling.consequent))) return true;
|
|
983
978
|
}
|
|
984
979
|
current = parent;
|
|
985
980
|
}
|
|
@@ -997,6 +992,11 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
997
992
|
"copyWithin"
|
|
998
993
|
]);
|
|
999
994
|
const createRule$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
995
|
+
function buildReadonlyArrayFix(annotated, fixer) {
|
|
996
|
+
if (annotated.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
997
|
+
if (annotated.type !== _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference) throw new Error(`Unreachable: expected a TSArrayType or TSTypeReference, got ${annotated.type} instead.`);
|
|
998
|
+
return fixer.replaceText(annotated.typeName, "ReadonlyArray");
|
|
999
|
+
}
|
|
1000
1000
|
function isUnionArrayType(typeAnnotation) {
|
|
1001
1001
|
if (typeAnnotation.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType && typeAnnotation.elementType.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return typeAnnotation.elementType;
|
|
1002
1002
|
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) {
|
|
@@ -1011,30 +1011,24 @@ const noMutableUnionArrayParam = createRule$5({
|
|
|
1011
1011
|
fixable: "code",
|
|
1012
1012
|
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." },
|
|
1013
1013
|
schema: [],
|
|
1014
|
-
messages: { unsound: "'{{ method }}' inserts into a parameter typed as an array of a union
|
|
1014
|
+
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." },
|
|
1015
|
+
defaultOptions: []
|
|
1015
1016
|
},
|
|
1016
|
-
defaultOptions: [],
|
|
1017
1017
|
create(context) {
|
|
1018
1018
|
return { CallExpression(node) {
|
|
1019
1019
|
const { callee } = node;
|
|
1020
|
-
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.
|
|
1020
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_INSERT_METHODS.has(callee.property.name)) return;
|
|
1021
1021
|
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);
|
|
1022
1022
|
if (!parameterDefinition) return;
|
|
1023
|
-
const parameterNode = parameterDefinition.name;
|
|
1024
|
-
if (
|
|
1025
|
-
|
|
1023
|
+
const parameterNode = asIdentifierName(parameterDefinition.name);
|
|
1024
|
+
if (!parameterNode.typeAnnotation) return;
|
|
1025
|
+
const annotated = parameterNode.typeAnnotation.typeAnnotation;
|
|
1026
|
+
if (!isUnionArrayType(annotated)) return;
|
|
1026
1027
|
context.report({
|
|
1027
1028
|
node,
|
|
1028
1029
|
messageId: "unsound",
|
|
1029
1030
|
data: { method: callee.property.name },
|
|
1030
|
-
fix(fixer)
|
|
1031
|
-
const typeAnnotation = parameterNode.typeAnnotation;
|
|
1032
|
-
if (!typeAnnotation) return null;
|
|
1033
|
-
const annotated = typeAnnotation.typeAnnotation;
|
|
1034
|
-
if (annotated.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
1035
|
-
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");
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1031
|
+
fix: (fixer) => buildReadonlyArrayFix(annotated, fixer)
|
|
1038
1032
|
});
|
|
1039
1033
|
} };
|
|
1040
1034
|
}
|
|
@@ -1050,8 +1044,7 @@ const noNonBarrelIndex = {
|
|
|
1050
1044
|
},
|
|
1051
1045
|
create(context) {
|
|
1052
1046
|
const filename = context.filename;
|
|
1053
|
-
const
|
|
1054
|
-
const basename = slash === -1 ? filename : filename.slice(slash + 1);
|
|
1047
|
+
const basename = filename.slice(filename.lastIndexOf("/") + 1);
|
|
1055
1048
|
if (!INDEX_BASENAME.test(basename)) return {};
|
|
1056
1049
|
if (filename.endsWith("/src/index.ts")) return {};
|
|
1057
1050
|
return { Program(node) {
|
|
@@ -1086,8 +1079,8 @@ const noNonBarrelReexport = {
|
|
|
1086
1079
|
fixable: "code",
|
|
1087
1080
|
schema: [],
|
|
1088
1081
|
messages: {
|
|
1089
|
-
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export
|
|
1090
|
-
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default`
|
|
1082
|
+
splitStatementReexport: "'{{ name }}' is imported here and handed straight back out via a bare export — the identical re-export 'export { {{ name }} } from ...' would be, just split across two statements. Re-exports belong only in the public barrel.",
|
|
1083
|
+
splitStatementDefaultReexport: "'{{ name }}' is imported here and handed straight back out via `export default` — the identical re-export 'export { {{ name }} as default } from ...' would be, just split across two statements. Re-exports belong only in the public barrel."
|
|
1091
1084
|
}
|
|
1092
1085
|
},
|
|
1093
1086
|
create(context) {
|
|
@@ -1118,14 +1111,14 @@ const noNonBarrelReexport = {
|
|
|
1118
1111
|
}
|
|
1119
1112
|
});
|
|
1120
1113
|
} else {
|
|
1121
|
-
const { declaration, name, trackedImport } = violation;
|
|
1114
|
+
const { declaration, identifierNode, name, trackedImport } = violation;
|
|
1122
1115
|
context.report({
|
|
1123
1116
|
node: declaration,
|
|
1124
1117
|
messageId: "splitStatementDefaultReexport",
|
|
1125
1118
|
data: { name },
|
|
1126
1119
|
fix(fixer) {
|
|
1127
1120
|
const fixes = [fixer.remove(declaration)];
|
|
1128
|
-
if (
|
|
1121
|
+
if (importIsOnlyUsedByThisExport(sourceCode, trackedImport, identifierNode)) fixes.push(removeListMember(fixer, sourceCode, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
1129
1122
|
return fixes;
|
|
1130
1123
|
}
|
|
1131
1124
|
});
|
|
@@ -1149,14 +1142,14 @@ const noObjectAssign = createRule$4({
|
|
|
1149
1142
|
type: "problem",
|
|
1150
1143
|
fixable: "code",
|
|
1151
1144
|
hasSuggestions: true,
|
|
1152
|
-
docs: { description: "Disallow Object.assign, whose own type declarations do not check a source object's properties against the target's declared types
|
|
1145
|
+
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." },
|
|
1153
1146
|
schema: [],
|
|
1154
1147
|
messages: {
|
|
1155
1148
|
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.",
|
|
1156
|
-
suggestSpreadReassign: "Replace with object spread and reassignment (changes the object reference
|
|
1157
|
-
}
|
|
1149
|
+
suggestSpreadReassign: "Replace with object spread and reassignment (changes the object reference — anything else already holding this object will not see the update)."
|
|
1150
|
+
},
|
|
1151
|
+
defaultOptions: []
|
|
1158
1152
|
},
|
|
1159
|
-
defaultOptions: [],
|
|
1160
1153
|
create(context) {
|
|
1161
1154
|
return { CallExpression(node) {
|
|
1162
1155
|
const { callee } = node;
|
|
@@ -1164,7 +1157,7 @@ const noObjectAssign = createRule$4({
|
|
|
1164
1157
|
const [target, ...sources] = node.arguments;
|
|
1165
1158
|
const sourcesAreSpreadable = sources.every((argument) => argument.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement);
|
|
1166
1159
|
if (target?.type === _typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression && sourcesAreSpreadable) {
|
|
1167
|
-
const isBareStatement = node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement
|
|
1160
|
+
const isBareStatement = node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement;
|
|
1168
1161
|
context.report({
|
|
1169
1162
|
node,
|
|
1170
1163
|
messageId: "unsound",
|
|
@@ -1177,7 +1170,7 @@ const noObjectAssign = createRule$4({
|
|
|
1177
1170
|
});
|
|
1178
1171
|
return;
|
|
1179
1172
|
}
|
|
1180
|
-
if (target?.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && sourcesAreSpreadable && node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement
|
|
1173
|
+
if (target?.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && sourcesAreSpreadable && node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement) {
|
|
1181
1174
|
const targetName = target.name;
|
|
1182
1175
|
const variable = resolveFrom$1(context.sourceCode.getScope(node), targetName);
|
|
1183
1176
|
const isConst = (variable?.defs.find((candidate) => candidate.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable))?.parent.kind === "const";
|
|
@@ -1210,7 +1203,16 @@ function isIdentifierReference(reference) {
|
|
|
1210
1203
|
return reference.identifier.type === "Identifier";
|
|
1211
1204
|
}
|
|
1212
1205
|
function hasTypeAnnotation(id) {
|
|
1213
|
-
return "typeAnnotation" in id && id.typeAnnotation !== void 0
|
|
1206
|
+
return "typeAnnotation" in id && id.typeAnnotation !== void 0;
|
|
1207
|
+
}
|
|
1208
|
+
function isConstDeclarator(declarator) {
|
|
1209
|
+
if (declarator.parent.type !== "VariableDeclaration") throw new Error(`Unreachable: expected a VariableDeclarator's own parent to be a VariableDeclaration, got "${declarator.parent.type}" instead.`);
|
|
1210
|
+
return declarator.parent.kind === "const";
|
|
1211
|
+
}
|
|
1212
|
+
function variableInScope(scope, name) {
|
|
1213
|
+
const variable = scope.set.get(name);
|
|
1214
|
+
if (variable === void 0) throw new Error(`Unreachable: expected scope.set to hold a variable named "${name}".`);
|
|
1215
|
+
return variable;
|
|
1214
1216
|
}
|
|
1215
1217
|
function resolveFrom(scope, name) {
|
|
1216
1218
|
for (let current = scope; current; current = current.upper) {
|
|
@@ -1218,6 +1220,18 @@ function resolveFrom(scope, name) {
|
|
|
1218
1220
|
if (found) return found;
|
|
1219
1221
|
}
|
|
1220
1222
|
}
|
|
1223
|
+
function hasEnclosingShorthandBoundary(precedingTokens) {
|
|
1224
|
+
for (const token of [...precedingTokens].reverse()) {
|
|
1225
|
+
if (token.value === "{" || token.value === ",") return true;
|
|
1226
|
+
if (token.value === "[" || token.value === "(" || token.value === ":") return false;
|
|
1227
|
+
}
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
function isShorthandPropertyRead(sourceCode, identifier) {
|
|
1231
|
+
const afterToken = sourceCode.getTokenAfter(identifier);
|
|
1232
|
+
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
1233
|
+
return hasEnclosingShorthandBoundary(sourceCode.getTokensBefore(identifier));
|
|
1234
|
+
}
|
|
1221
1235
|
const noPointlessReassignment = {
|
|
1222
1236
|
meta: {
|
|
1223
1237
|
type: "problem",
|
|
@@ -1228,7 +1242,7 @@ const noPointlessReassignment = {
|
|
|
1228
1242
|
create(context) {
|
|
1229
1243
|
return { VariableDeclarator(node) {
|
|
1230
1244
|
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
1231
|
-
if (node
|
|
1245
|
+
if (!isConstDeclarator(node)) return;
|
|
1232
1246
|
const scope = context.sourceCode.getScope(node);
|
|
1233
1247
|
const sourceVariable = scope.references.find((reference) => reference.identifier === node.init)?.resolved;
|
|
1234
1248
|
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && reference.init !== true)) return;
|
|
@@ -1243,24 +1257,13 @@ const noPointlessReassignment = {
|
|
|
1243
1257
|
value: originalName
|
|
1244
1258
|
},
|
|
1245
1259
|
fix(fixer) {
|
|
1246
|
-
const variable = scope
|
|
1247
|
-
if (!variable) return null;
|
|
1260
|
+
const variable = variableInScope(scope, aliasName);
|
|
1248
1261
|
if (aliasIsAnnotated) return null;
|
|
1249
1262
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
1255
|
-
let token = context.sourceCode.getTokenBefore(reference.identifier);
|
|
1256
|
-
while (token) {
|
|
1257
|
-
if (token.value === "{") return true;
|
|
1258
|
-
if (token.value === "[" || token.value === "(") return false;
|
|
1259
|
-
if (token.value === ":") return false;
|
|
1260
|
-
token = context.sourceCode.getTokenBefore(token);
|
|
1261
|
-
}
|
|
1262
|
-
return false;
|
|
1263
|
-
})) return null;
|
|
1263
|
+
const readReferences = variable.references.filter((reference) => reference.isRead());
|
|
1264
|
+
const readRefs = readReferences.filter(isIdentifierReference);
|
|
1265
|
+
if (readRefs.length !== readReferences.length) return null;
|
|
1266
|
+
if (readRefs.some((reference) => isShorthandPropertyRead(context.sourceCode, reference.identifier))) return null;
|
|
1264
1267
|
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
1265
1268
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
1266
1269
|
const declaration = node.parent;
|
|
@@ -1297,7 +1300,7 @@ const noSetInstanceofMutation = createRule$3({
|
|
|
1297
1300
|
type: "problem",
|
|
1298
1301
|
schema: [],
|
|
1299
1302
|
docs: { description: "Disallow mutating calls on a parameter or local variable whose real type includes a ReadonlySet, narrowed via instanceof Set, which silently discards the declared read-only guarantee." },
|
|
1300
|
-
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by instanceof Set
|
|
1303
|
+
messages: { unsound: "'{{ method }}' mutates a parameter or local variable narrowed by instanceof Set — instanceof Set's own narrowing widens straight to the mutable Set interface, so a value whose real type includes a ReadonlySet (a caller's set, for a parameter; the value's own declared type, for a local variable) can be mutated here despite that readonly guarantee. Copy the set before mutating (e.g. new Set(input)), or narrow with a check that preserves read-only instead of instanceof Set." }
|
|
1301
1304
|
},
|
|
1302
1305
|
defaultOptions: [],
|
|
1303
1306
|
create(context) {
|
|
@@ -1310,14 +1313,12 @@ const noSetInstanceofMutation = createRule$3({
|
|
|
1310
1313
|
}
|
|
1311
1314
|
return { CallExpression(node) {
|
|
1312
1315
|
const { callee } = node;
|
|
1313
|
-
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.
|
|
1316
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !MUTATING_SET_METHODS.has(callee.property.name)) return;
|
|
1314
1317
|
const variable = context.sourceCode.getScope(node).references.find((reference) => reference.identifier === callee.object)?.resolved;
|
|
1315
1318
|
if (!variable) return;
|
|
1316
1319
|
const declarationDefinition = variable.defs.find((definition) => definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Parameter || definition.type === _typescript_eslint_utils.TSESLint.Scope.DefinitionType.Variable);
|
|
1317
1320
|
if (!declarationDefinition) return;
|
|
1318
|
-
|
|
1319
|
-
if (declarationNode.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return;
|
|
1320
|
-
if (!declarationHasReadonlySetConstituent(declarationNode)) return;
|
|
1321
|
+
if (!declarationHasReadonlySetConstituent(asIdentifierName(declarationDefinition.name))) return;
|
|
1321
1322
|
if (!isGuardedBySetInstanceof(node, variable, context)) return;
|
|
1322
1323
|
context.report({
|
|
1323
1324
|
node,
|
|
@@ -1345,7 +1346,7 @@ const noSetInstanceofMutation = createRule$3({
|
|
|
1345
1346
|
if (parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
1346
1347
|
if (parent.alternate === current && isNegatedSetInstanceofExpression(parent.test, parameterVariable, ruleContext)) return true;
|
|
1347
1348
|
}
|
|
1348
|
-
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" &&
|
|
1349
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression && parent.operator === "&&" && matchesSetInstanceofOn(parent.left, parameterVariable, ruleContext)) return true;
|
|
1349
1350
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression && parent.consequent === current && matchesSetInstanceofOn(parent.test, parameterVariable, ruleContext)) return true;
|
|
1350
1351
|
current = parent;
|
|
1351
1352
|
}
|
|
@@ -1357,15 +1358,7 @@ const noSetInstanceofMutation = createRule$3({
|
|
|
1357
1358
|
const { parent } = current;
|
|
1358
1359
|
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Program) {
|
|
1359
1360
|
const statements = parent.body;
|
|
1360
|
-
|
|
1361
|
-
for (let i = 0; i < statements.length; i++) if (statements[i] === current) {
|
|
1362
|
-
ownIndex = i;
|
|
1363
|
-
break;
|
|
1364
|
-
}
|
|
1365
|
-
if (ownIndex > 0) for (let i = ownIndex - 1; i >= 0; i--) {
|
|
1366
|
-
const sibling = statements[i];
|
|
1367
|
-
if (sibling?.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedSetInstanceofExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent)) return true;
|
|
1368
|
-
}
|
|
1361
|
+
if (statements.slice(0, statements.indexOf(current)).some((sibling) => sibling.type === _typescript_eslint_utils.AST_NODE_TYPES.IfStatement && !sibling.alternate && isNegatedSetInstanceofExpression(sibling.test, parameterVariable, ruleContext) && definitelyExits(sibling.consequent))) return true;
|
|
1369
1362
|
}
|
|
1370
1363
|
current = parent;
|
|
1371
1364
|
}
|
|
@@ -1379,7 +1372,7 @@ const noSideEffectsInIndex = {
|
|
|
1379
1372
|
meta: {
|
|
1380
1373
|
type: "problem",
|
|
1381
1374
|
schema: [],
|
|
1382
|
-
messages: { notAPureReexport: "A barrel (index) file may contain only re-export statements ('export * from ...' / 'export { x } from ...' / 'export type { x } from ...')
|
|
1375
|
+
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 }}." }
|
|
1383
1376
|
},
|
|
1384
1377
|
create(context) {
|
|
1385
1378
|
if (!isIndexFile(context.filename)) return {};
|
|
@@ -1410,6 +1403,12 @@ const DEFAULT_SORT_AZ = [
|
|
|
1410
1403
|
"resolutions",
|
|
1411
1404
|
"scripts"
|
|
1412
1405
|
];
|
|
1406
|
+
/** Reads `array[index]`, throwing loudly instead of silently returning `undefined` when it is genuinely out of bounds. Every call site in this file passes an index that is provably in range by construction (a loop bound, a length check, or a mathematical invariant established immediately above the call) — this exists to satisfy `noUncheckedIndexedAccess` with a real runtime guarantee instead of a silent `as` cast or a `?? fallback` that could quietly paper over a genuine bug, and none of the arrays this file indexes (keys, nodes, ranges, characters) ever legitimately store `undefined` as an element. Exported so each call site's own "this can never actually throw" claim is checked directly against a deliberately out-of-bounds input, rather than trusted on the strength of a comment alone. */
|
|
1407
|
+
function at(array, index) {
|
|
1408
|
+
const value = array[index];
|
|
1409
|
+
if (value === void 0) throw new Error(`Unreachable: index ${String(index)} is out of bounds for an array of length ${String(array.length)}.`);
|
|
1410
|
+
return value;
|
|
1411
|
+
}
|
|
1413
1412
|
function categorize(char) {
|
|
1414
1413
|
if (char >= "0" && char <= "9") return 1;
|
|
1415
1414
|
if (/[a-z]/iu.test(char)) return 2;
|
|
@@ -1419,14 +1418,15 @@ function categorize(char) {
|
|
|
1419
1418
|
function compareSyncpackKey(a, b) {
|
|
1420
1419
|
const length = Math.min(a.length, b.length);
|
|
1421
1420
|
for (let index = 0; index < length; index += 1) {
|
|
1422
|
-
const charA = a
|
|
1423
|
-
const charB = b
|
|
1421
|
+
const charA = a.charAt(index);
|
|
1422
|
+
const charB = b.charAt(index);
|
|
1424
1423
|
const categoryA = categorize(charA);
|
|
1425
1424
|
const categoryB = categorize(charB);
|
|
1426
1425
|
if (categoryA !== categoryB) return categoryA - categoryB;
|
|
1427
1426
|
const foldedA = categoryA === 2 ? charA.toLowerCase() : charA;
|
|
1428
1427
|
const foldedB = categoryB === 2 ? charB.toLowerCase() : charB;
|
|
1429
|
-
if (foldedA
|
|
1428
|
+
if (foldedA < foldedB) return -1;
|
|
1429
|
+
if (foldedA > foldedB) return 1;
|
|
1430
1430
|
}
|
|
1431
1431
|
return a.length - b.length;
|
|
1432
1432
|
}
|
|
@@ -1446,16 +1446,16 @@ function isValidSortAzOrder(prev, curr) {
|
|
|
1446
1446
|
return compareSyncpackKey(prev, curr) <= 0;
|
|
1447
1447
|
}
|
|
1448
1448
|
/**
|
|
1449
|
-
* The target permutation for a whole container's own keys, given a comparator over adjacent pairs (either `isValidTopLevelOrder`-shaped or `isValidSortAzOrder`-shaped). Returns, for each output position, the ORIGINAL index of the key that belongs there
|
|
1449
|
+
* The target permutation for a whole container's own keys, given a comparator over adjacent pairs (either `isValidTopLevelOrder`-shaped or `isValidSortAzOrder`-shaped). Returns, for each output position, the ORIGINAL index of the key that belongs there — e.g. `[2, 0, 1]` means the key currently at index 2 comes first. A stable sort (ties keep their original relative order), matching real syncpack output for keys that compare equal (see `compareSyncpackKey`'s own case-insensitive-letter test case).
|
|
1450
1450
|
*
|
|
1451
|
-
* Reordering by computing this full permutation once, rather than reporting and fixing one out-of-order adjacent pair at a time (the technique `@eslint/json`'s own `sort-keys` rule uses), is deliberate: an adjacent-swap fixer only makes one pass of bubble-sort-style progress per lint pass, and a realistically scrambled `package.json` (many top-level keys all needing to move several positions, several nested objects also needing sorting) was confirmed directly to need up to 11 fix passes to fully converge
|
|
1451
|
+
* Reordering by computing this full permutation once, rather than reporting and fixing one out-of-order adjacent pair at a time (the technique `@eslint/json`'s own `sort-keys` rule uses), is deliberate: an adjacent-swap fixer only makes one pass of bubble-sort-style progress per lint pass, and a realistically scrambled `package.json` (many top-level keys all needing to move several positions, several nested objects also needing sorting) was confirmed directly to need up to 11 fix passes to fully converge — one more than ESLint's own `Linter#verifyAndFix` will ever run (`MAX_AUTOFIX_PASSES` is 10), meaning a single real `eslint --fix` invocation could leave such a file only partially reordered. Computing the whole target order up front and replacing every affected key's position in one combined fix converges in a single pass regardless of how scrambled the input is.
|
|
1452
1452
|
*/
|
|
1453
1453
|
function computeOrderPermutation(keys, isValidOrder) {
|
|
1454
1454
|
return keys.map((_, index) => index).map((index) => ({
|
|
1455
|
-
key: keys
|
|
1455
|
+
key: at(keys, index),
|
|
1456
1456
|
index
|
|
1457
1457
|
})).sort((a, b) => {
|
|
1458
|
-
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return
|
|
1458
|
+
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return 0;
|
|
1459
1459
|
return isValidOrder(a.key, b.key) ? -1 : 1;
|
|
1460
1460
|
}).map((entry) => entry.index);
|
|
1461
1461
|
}
|
|
@@ -1465,17 +1465,27 @@ function isIdentityPermutation(permutation) {
|
|
|
1465
1465
|
function isStringNode(node) {
|
|
1466
1466
|
return node.type === "String";
|
|
1467
1467
|
}
|
|
1468
|
+
function isStringNamed(name) {
|
|
1469
|
+
return name.type === "String";
|
|
1470
|
+
}
|
|
1468
1471
|
function getMemberKeyName(member) {
|
|
1469
|
-
|
|
1472
|
+
const { name } = member;
|
|
1473
|
+
if (!isStringNamed(name)) throw new Error(`Unreachable: package-json-key-order only supports json/json and json/jsonc, where an unquoted (Identifier) member name is a parse error — got a "${name.type}" name instead.`);
|
|
1474
|
+
return name.value;
|
|
1475
|
+
}
|
|
1476
|
+
function rangeOf(node) {
|
|
1477
|
+
if (node.range === void 0) throw new Error("Unreachable: every node reaching this function was produced by a JSON language that always requests range tracking.");
|
|
1478
|
+
return node.range;
|
|
1470
1479
|
}
|
|
1471
|
-
function
|
|
1472
|
-
|
|
1480
|
+
function objectParentOrThrow(parent) {
|
|
1481
|
+
if (parent === void 0) throw new Error("Unreachable: the Object visitor is never invoked for the traversal root, which is always the Document node.");
|
|
1482
|
+
return parent;
|
|
1473
1483
|
}
|
|
1474
1484
|
const commentTypes = /* @__PURE__ */ new Set(["LineComment", "BlockComment"]);
|
|
1475
1485
|
/**
|
|
1476
|
-
* Enforces the same `package.json` key order `syncpack format` would produce: `sortFirst` fields pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value, its members/elements sorted the same way (no pinning there
|
|
1486
|
+
* Enforces the same `package.json` key order `syncpack format` would produce: `sortFirst` fields pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value, its members/elements sorted the same way (no pinning there — `sortFirst` only ever applies at the top level).
|
|
1477
1487
|
*
|
|
1478
|
-
* This exists so a project using `@exadev/eslint-config` without syncpack still gets real `package.json` canonicalization, and so a project using both never sees them fight: `eslint --fix` and `syncpack format` converge on the identical output, confirmed directly against real `syncpack@15` output rather than assumed from its docs (see `compareSyncpackKey`'s own comment). Deliberately narrower than `@eslint/json`'s own `sort-keys` rule, which only supports a single alphabetical order for every key with no field-specific pinning
|
|
1488
|
+
* This exists so a project using `@exadev/eslint-config` without syncpack still gets real `package.json` canonicalization, and so a project using both never sees them fight: `eslint --fix` and `syncpack format` converge on the identical output, confirmed directly against real `syncpack@15` output rather than assumed from its docs (see `compareSyncpackKey`'s own comment). Deliberately narrower than `@eslint/json`'s own `sort-keys` rule, which only supports a single alphabetical order for every key with no field-specific pinning — not a fit for syncpack's own two-tier scheme.
|
|
1479
1489
|
*
|
|
1480
1490
|
* Not part of `plugin.configs.recommended`: opt-in, the same tri-state shape as `react`/`nextjs` (see `docs.md`'s own usage example), since a project without syncpack needs to choose this deliberately, and a project with syncpack needs to confirm the two tools agree before turning it on.
|
|
1481
1491
|
*/
|
|
@@ -1513,65 +1523,42 @@ const packageJsonKeyOrder = {
|
|
|
1513
1523
|
const { sourceCode } = context;
|
|
1514
1524
|
function hasAdjacentComment(node) {
|
|
1515
1525
|
const before = sourceCode.getTokenBefore(node, { includeComments: true });
|
|
1516
|
-
|
|
1517
|
-
if (after?.type === "Comma") after = sourceCode.getTokenAfter(after, { includeComments: true });
|
|
1526
|
+
const after = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
1518
1527
|
return before !== null && commentTypes.has(before.type) || after !== null && commentTypes.has(after.type);
|
|
1519
1528
|
}
|
|
1520
1529
|
function buildReorderedText(nodes, permutation) {
|
|
1521
1530
|
const separators = nodes.slice(0, -1).map((node, index) => {
|
|
1522
|
-
const next = nodes
|
|
1523
|
-
return
|
|
1531
|
+
const next = at(nodes, index + 1);
|
|
1532
|
+
return sourceCode.text.slice(rangeOf(node)[1], rangeOf(next)[0]);
|
|
1524
1533
|
});
|
|
1525
|
-
return permutation.reduce((result,
|
|
1526
|
-
const text = sourceCode.getText(nodes[originalIndex]);
|
|
1527
|
-
return position === 0 ? text : `${result}${separators[position - 1] ?? ""}${text}`;
|
|
1528
|
-
}, "");
|
|
1534
|
+
return permutation.map((originalIndex) => sourceCode.getText(at(nodes, originalIndex))).reduce((result, text, position) => `${result}${at(separators, position - 1)}${text}`);
|
|
1529
1535
|
}
|
|
1530
1536
|
function getNodeRanges(nodes) {
|
|
1531
|
-
|
|
1532
|
-
for (const node of nodes) {
|
|
1533
|
-
if (!hasRange(node)) return void 0;
|
|
1534
|
-
ranges.push(node.range);
|
|
1535
|
-
}
|
|
1536
|
-
return ranges;
|
|
1537
|
+
return nodes.map((node) => rangeOf(node));
|
|
1537
1538
|
}
|
|
1538
1539
|
function reportWholeContainerReorder(nodes, keys, isValidOrder, reportLoc) {
|
|
1539
1540
|
const permutation = computeOrderPermutation(keys, isValidOrder);
|
|
1540
1541
|
if (isIdentityPermutation(permutation)) return;
|
|
1541
|
-
const
|
|
1542
|
-
const lastNode = nodes[nodes.length - 1];
|
|
1543
|
-
if (firstNode === void 0 || lastNode === void 0) return;
|
|
1544
|
-
let violationIndex = 1;
|
|
1545
|
-
for (let index = 1; index < keys.length; index += 1) if (!isValidOrder(keys[index - 1] ?? "", keys[index] ?? "")) {
|
|
1546
|
-
violationIndex = index;
|
|
1547
|
-
break;
|
|
1548
|
-
}
|
|
1542
|
+
const violationIndex = keys.findIndex((curr, index) => index > 0 && !isValidOrder(at(keys, index - 1), curr));
|
|
1549
1543
|
context.report({
|
|
1550
|
-
loc: reportLoc(nodes
|
|
1544
|
+
loc: reportLoc(at(nodes, violationIndex)),
|
|
1551
1545
|
messageId: "outOfOrder",
|
|
1552
1546
|
data: {
|
|
1553
|
-
curr: keys
|
|
1554
|
-
prev: keys
|
|
1547
|
+
curr: at(keys, violationIndex),
|
|
1548
|
+
prev: at(keys, violationIndex - 1)
|
|
1555
1549
|
},
|
|
1556
1550
|
fix(fixer) {
|
|
1551
|
+
if (nodes.some((node) => hasAdjacentComment(node))) return null;
|
|
1557
1552
|
const ranges = getNodeRanges(nodes);
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
const lastRange = ranges[ranges.length - 1];
|
|
1561
|
-
if (firstRange === void 0 || lastRange === void 0) return null;
|
|
1553
|
+
const firstRange = at(ranges, 0);
|
|
1554
|
+
const lastRange = at(ranges, ranges.length - 1);
|
|
1562
1555
|
const rewritten = buildReorderedText(nodes, permutation);
|
|
1563
1556
|
return fixer.replaceTextRange([firstRange[0], lastRange[1]], rewritten);
|
|
1564
1557
|
}
|
|
1565
1558
|
});
|
|
1566
1559
|
}
|
|
1567
1560
|
function checkMembers(members, isValidOrder) {
|
|
1568
|
-
|
|
1569
|
-
for (const member of members) {
|
|
1570
|
-
const key = getMemberKeyName(member);
|
|
1571
|
-
if (key === void 0) return;
|
|
1572
|
-
keys.push(key);
|
|
1573
|
-
}
|
|
1574
|
-
reportWholeContainerReorder(members, keys, isValidOrder, (member) => member.name.loc.start);
|
|
1561
|
+
reportWholeContainerReorder(members, members.map((member) => getMemberKeyName(member)), isValidOrder, (member) => member.name.loc.start);
|
|
1575
1562
|
}
|
|
1576
1563
|
function checkElements(elements) {
|
|
1577
1564
|
const stringValues = [];
|
|
@@ -1582,21 +1569,19 @@ const packageJsonKeyOrder = {
|
|
|
1582
1569
|
reportWholeContainerReorder(stringValues, stringValues.map((value) => value.value), isValidSortAzOrder, (value) => value.loc.start);
|
|
1583
1570
|
}
|
|
1584
1571
|
return {
|
|
1585
|
-
Object(node,
|
|
1586
|
-
|
|
1572
|
+
Object(node, rawParent) {
|
|
1573
|
+
const parent = objectParentOrThrow(rawParent);
|
|
1587
1574
|
if (parent.type === "Document") {
|
|
1588
1575
|
checkMembers(node.members, (prev, curr) => isValidTopLevelOrder(prev, curr, sortFirst));
|
|
1589
1576
|
return;
|
|
1590
1577
|
}
|
|
1591
1578
|
if (parent.type === "Member") {
|
|
1592
|
-
|
|
1593
|
-
if (key !== void 0 && sortAz.has(key)) checkMembers(node.members, isValidSortAzOrder);
|
|
1579
|
+
if (sortAz.has(getMemberKeyName(parent))) checkMembers(node.members, isValidSortAzOrder);
|
|
1594
1580
|
}
|
|
1595
1581
|
},
|
|
1596
1582
|
Array(node, parent) {
|
|
1597
1583
|
if (parent?.type !== "Member") return;
|
|
1598
|
-
|
|
1599
|
-
if (key !== void 0 && sortAz.has(key)) checkElements(node.elements);
|
|
1584
|
+
if (sortAz.has(getMemberKeyName(parent))) checkElements(node.elements);
|
|
1600
1585
|
}
|
|
1601
1586
|
};
|
|
1602
1587
|
}
|
|
@@ -1614,14 +1599,14 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1614
1599
|
meta: {
|
|
1615
1600
|
type: "suggestion",
|
|
1616
1601
|
hasSuggestions: true,
|
|
1617
|
-
docs: { description: "Suggest an ascending numeric compare function for a bare '.sort()'/'.toSorted()' call on an array whose element type is definitively 'number'
|
|
1602
|
+
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." },
|
|
1618
1603
|
schema: [],
|
|
1619
1604
|
messages: {
|
|
1620
1605
|
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.",
|
|
1621
1606
|
addAscendingCompare: "Add an ascending numeric compare function: '(a, b) => a - b'."
|
|
1622
|
-
}
|
|
1607
|
+
},
|
|
1608
|
+
defaultOptions: []
|
|
1623
1609
|
},
|
|
1624
|
-
defaultOptions: [],
|
|
1625
1610
|
create(context) {
|
|
1626
1611
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
1627
1612
|
const checker = services.program.getTypeChecker();
|
|
@@ -1630,12 +1615,10 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1630
1615
|
const { callee } = node;
|
|
1631
1616
|
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed) return;
|
|
1632
1617
|
if (callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || !SORT_METHOD_NAMES.has(callee.property.name)) return;
|
|
1633
|
-
const receiverTsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
|
|
1634
|
-
if (!typescript.isExpression(receiverTsNode)) return;
|
|
1618
|
+
const receiverTsNode = asExpression(services.esTreeNodeToTSNodeMap.get(callee.object));
|
|
1635
1619
|
const receiverType = checker.getTypeAtLocation(receiverTsNode);
|
|
1636
1620
|
if (!checker.isArrayType(receiverType)) return;
|
|
1637
|
-
|
|
1638
|
-
const [elementType] = checker.getTypeArguments(receiverType);
|
|
1621
|
+
const [elementType] = checker.getTypeArguments(asTypeReference(receiverType));
|
|
1639
1622
|
if (!elementType || !isDefinitelyNumberType(elementType)) return;
|
|
1640
1623
|
context.report({
|
|
1641
1624
|
node,
|
|
@@ -1644,8 +1627,7 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1644
1627
|
suggest: [{
|
|
1645
1628
|
messageId: "addAscendingCompare",
|
|
1646
1629
|
fix(fixer) {
|
|
1647
|
-
const closingParen = context.sourceCode
|
|
1648
|
-
if (!closingParen) return null;
|
|
1630
|
+
const closingParen = lastTokenOrThrow(context.sourceCode, node);
|
|
1649
1631
|
return fixer.insertTextBefore(closingParen, "(a, b) => a - b");
|
|
1650
1632
|
}
|
|
1651
1633
|
}]
|
|
@@ -1657,7 +1639,6 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1657
1639
|
//#region src/rules/prefer-readonly-array-param.ts
|
|
1658
1640
|
const createRule$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1659
1641
|
function getFixableArrayOrTupleType(typeNode) {
|
|
1660
|
-
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeOperator && typeNode.operator === "readonly") return void 0;
|
|
1661
1642
|
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return typeNode;
|
|
1662
1643
|
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTupleType) return typeNode;
|
|
1663
1644
|
if (typeNode.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference && typeNode.typeName.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && typeNode.typeName.name === "Array") return typeNode;
|
|
@@ -1688,11 +1669,11 @@ const preferReadonlyArrayParam = createRule$1({
|
|
|
1688
1669
|
meta: {
|
|
1689
1670
|
type: "problem",
|
|
1690
1671
|
fixable: "code",
|
|
1691
|
-
docs: { description: "Require array and tuple parameters to be typed readonly, regardless of whether the function body mutates them
|
|
1672
|
+
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." },
|
|
1692
1673
|
schema: [],
|
|
1693
|
-
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." }
|
|
1674
|
+
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." },
|
|
1675
|
+
defaultOptions: []
|
|
1694
1676
|
},
|
|
1695
|
-
defaultOptions: [],
|
|
1696
1677
|
create(context) {
|
|
1697
1678
|
function checkParam(param) {
|
|
1698
1679
|
const annotatedNode = getAnnotatedParamNode$1(param);
|
|
@@ -1746,15 +1727,18 @@ function isFlatPropertyType(checker, type) {
|
|
|
1746
1727
|
if ((type.flags & PRIMITIVE_LIKE_FLAGS) !== 0) return true;
|
|
1747
1728
|
return checker.getSignaturesOfType(type, typescript.SignatureKind.Call).length > 0 && checker.getPropertiesOfType(type).length === 0 && checker.getIndexInfosOfType(type).length === 0;
|
|
1748
1729
|
}
|
|
1730
|
+
function hasSymbolFlag(type, flag) {
|
|
1731
|
+
return ((type.getSymbol()?.flags ?? 0) & flag) !== 0;
|
|
1732
|
+
}
|
|
1749
1733
|
function isFlatObjectType(checker, type, location) {
|
|
1750
1734
|
if ((type.flags & typescript.TypeFlags.Object) === 0) return false;
|
|
1751
|
-
if (type.flags & (typescript.TypeFlags.Union | typescript.TypeFlags.Intersection | typescript.TypeFlags.TypeParameter)) return false;
|
|
1752
|
-
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
|
1753
1735
|
if (checker.getSignaturesOfType(type, typescript.SignatureKind.Call).length > 0) return false;
|
|
1754
1736
|
if (checker.getSignaturesOfType(type, typescript.SignatureKind.Construct).length > 0) return false;
|
|
1755
|
-
if ((type
|
|
1737
|
+
if (hasSymbolFlag(type, typescript.SymbolFlags.Class)) return false;
|
|
1756
1738
|
const symbolName = type.getSymbol()?.name;
|
|
1739
|
+
if (symbolName === "Date") return false;
|
|
1757
1740
|
if (symbolName === "Map" || symbolName === "ReadonlyMap" || symbolName === "Set" || symbolName === "ReadonlySet") return false;
|
|
1741
|
+
if (symbolName === "WeakMap" || symbolName === "WeakSet") return false;
|
|
1758
1742
|
for (const property of checker.getPropertiesOfType(type)) if (!isFlatPropertyType(checker, checker.getTypeOfSymbolAtLocation(property, location))) return false;
|
|
1759
1743
|
for (const indexInfo of checker.getIndexInfosOfType(type)) if (!isFlatPropertyType(checker, indexInfo.type)) return false;
|
|
1760
1744
|
return true;
|
|
@@ -1770,11 +1754,11 @@ const preferReadonlyObjectParam = createRule({
|
|
|
1770
1754
|
meta: {
|
|
1771
1755
|
type: "problem",
|
|
1772
1756
|
fixable: "code",
|
|
1773
|
-
docs: { description: "Require a 'flat' object parameter (every property is a primitive or a callback, so a shallow wrapper is provably sufficient) to be typed 'Readonly<T>'
|
|
1757
|
+
docs: { description: "Require a 'flat' object parameter (every property is a primitive or a callback, so a shallow wrapper is provably sufficient) to be typed 'Readonly<T>' — a narrower, safely-autofixable sibling of @typescript-eslint/prefer-readonly-parameter-types and of this package's own prefer-readonly-array-param, scoped to the object shapes where a shallow fix is genuinely complete." },
|
|
1774
1758
|
schema: [],
|
|
1775
|
-
messages: { preferReadonlyObject: "This object parameter is 'flat'
|
|
1759
|
+
messages: { preferReadonlyObject: "This object parameter is 'flat' — every property (and index-signature value, if any) is a primitive or a callback, so there is no nested mutable state a shallow wrapper could miss. Wrap it in 'Readonly<...>' so a caller can pass a readonly or shared object with confidence, and so any attempt to mutate it inside the function becomes a deliberate, visible compile error instead of a silent side effect on the caller's data." },
|
|
1760
|
+
defaultOptions: []
|
|
1776
1761
|
},
|
|
1777
|
-
defaultOptions: [],
|
|
1778
1762
|
create(context) {
|
|
1779
1763
|
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
1780
1764
|
const checker = services.program.getTypeChecker();
|
|
@@ -1896,14 +1880,13 @@ function resolveJsonPlugin(value) {
|
|
|
1896
1880
|
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
1897
1881
|
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
1898
1882
|
}
|
|
1899
|
-
const JSON_PLUGIN_INSTALL_COMMAND = "pnpm add -D @eslint/json";
|
|
1900
1883
|
function buildPackageJsonKeyOrderConfig(options = {}) {
|
|
1901
1884
|
const cwd = options.cwd ?? process.cwd();
|
|
1902
1885
|
if (options.enabled === false) return [];
|
|
1903
1886
|
if (options.enabled === void 0 && hasSyncpackConfig(cwd)) return [];
|
|
1904
1887
|
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", options.requireFn));
|
|
1905
1888
|
if (jsonPlugin === void 0) {
|
|
1906
|
-
if (options.enabled === true) throw new Error(`@exadev/eslint-config: package.json key ordering was explicitly requested but '@eslint/json' could not be resolved. Install it with:
|
|
1889
|
+
if (options.enabled === true) throw new Error(`@exadev/eslint-config: package.json key ordering was explicitly requested but '@eslint/json' could not be resolved. Install it with: pnpm add -D @eslint/json`);
|
|
1907
1890
|
return [];
|
|
1908
1891
|
}
|
|
1909
1892
|
const ruleOptions = {
|