eslint-plugin-ts-data-forge 0.8.1 → 0.8.3
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/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +3 -3
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +59 -3
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +142 -31
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/branded-number-types.mjs.map +1 -1
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs +26 -13
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +1 -1
- package/dist/rules/no-side-effect-import.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +25 -11
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +11 -21
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +16 -28
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +16 -24
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +16 -24
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +9 -22
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts +6 -5
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +68 -31
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +57 -40
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +59 -30
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +27 -24
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +59 -28
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +41 -16
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +18 -8
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs +31 -6
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +56 -40
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +18 -8
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +15 -8
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +25 -23
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +3 -3
- package/src/rules/ast-utils.mts +192 -37
- package/src/rules/branded-number-types.mts +1 -1
- package/src/rules/import-utils.mts +41 -17
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/no-unnecessary-type-guard.mts +249 -148
- package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
- package/src/rules/prefer-arr-is-array.mts +32 -13
- package/src/rules/prefer-arr-is-array.test.mts +40 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
- package/src/rules/prefer-arr-is-non-empty.mts +19 -24
- package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
- package/src/rules/prefer-arr-scan.mts +107 -37
- package/src/rules/prefer-arr-scan.test.mts +149 -0
- package/src/rules/prefer-arr-sum.mts +87 -50
- package/src/rules/prefer-arr-sum.test.mts +97 -0
- package/src/rules/prefer-arr-uniq.mts +83 -33
- package/src/rules/prefer-arr-uniq.test.mts +71 -0
- package/src/rules/prefer-as-int.mts +38 -38
- package/src/rules/prefer-as-int.test.mts +51 -0
- package/src/rules/prefer-canonical-array-slicing.mts +84 -29
- package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
- package/src/rules/prefer-canonical-length-cast.mts +56 -18
- package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
- package/src/rules/prefer-canonical-length-guard.mts +25 -11
- package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
- package/src/rules/prefer-is-non-null-object.mts +57 -33
- package/src/rules/prefer-is-non-null-object.test.mts +42 -0
- package/src/rules/prefer-is-record-and-has-key.mts +27 -12
- package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
- package/src/rules/prefer-num-safe-parse-float.mts +37 -13
- package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
- package/src/rules/prefer-num-safe-parse-int.mts +41 -15
- package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
- package/src/rules/prefer-range-for-loop.mts +21 -9
- package/src/rules/prefer-range-for-loop.test.mts +75 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
type TSESLint,
|
|
5
5
|
type TSESTree,
|
|
6
6
|
} from '@typescript-eslint/utils';
|
|
7
|
-
import {
|
|
7
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
8
8
|
import * as ts from 'typescript';
|
|
9
|
+
import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
|
|
9
10
|
import {
|
|
10
11
|
buildImportFixes,
|
|
11
12
|
getImportedLocalName,
|
|
@@ -48,19 +49,24 @@ const normalizeCountedOp = (
|
|
|
48
49
|
: ({ fn, count } as const);
|
|
49
50
|
|
|
50
51
|
const asNonNegativeIntegerLiteral = (
|
|
51
|
-
|
|
52
|
-
): number | undefined =>
|
|
53
|
-
node
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
wrapped: TSESTree.Node,
|
|
53
|
+
): number | undefined => {
|
|
54
|
+
const node = skipTypeWrappers(wrapped);
|
|
55
|
+
|
|
56
|
+
return node.type === AST_NODE_TYPES.Literal &&
|
|
57
|
+
typeof node.value === 'number' &&
|
|
58
|
+
Number.isInteger(node.value) &&
|
|
59
|
+
node.value >= 0
|
|
57
60
|
? node.value
|
|
58
61
|
: undefined;
|
|
62
|
+
};
|
|
59
63
|
|
|
60
64
|
/** Matches `-N` (unary minus of a positive integer literal) and returns `N`. */
|
|
61
65
|
const asNegatedPositiveIntegerLiteral = (
|
|
62
|
-
|
|
66
|
+
wrapped: TSESTree.Node,
|
|
63
67
|
): number | undefined => {
|
|
68
|
+
const node = skipTypeWrappers(wrapped);
|
|
69
|
+
|
|
64
70
|
if (
|
|
65
71
|
node.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
66
72
|
node.operator !== '-' ||
|
|
@@ -238,6 +244,18 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
238
244
|
const getTypeOf = (node: TSESTree.Node): ts.Type =>
|
|
239
245
|
checker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node));
|
|
240
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Whether `predicate` holds for the type of every layer of type wrappers
|
|
249
|
+
* around `node` (see typeWrapperLayers): a cast neither makes a `Set` an
|
|
250
|
+
* array nor stops an array from being flattened by `concat`, and the fix,
|
|
251
|
+
* which keeps the wrapper, has to type-check as well.
|
|
252
|
+
*/
|
|
253
|
+
const holdsThroughWrappers = (
|
|
254
|
+
node: TSESTree.Node,
|
|
255
|
+
predicate: (type: ts.Type) => boolean,
|
|
256
|
+
): boolean =>
|
|
257
|
+
typeWrapperLayers(node).every((layer) => predicate(getTypeOf(layer)));
|
|
258
|
+
|
|
241
259
|
/**
|
|
242
260
|
* `true` only when the type can never be an array in any branch, so
|
|
243
261
|
* `xs.concat(v)` is guaranteed to append `v` as one element rather than
|
|
@@ -395,9 +413,14 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
395
413
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
396
414
|
const rawOperator = body.operator as ComparisonOperator;
|
|
397
415
|
|
|
398
|
-
const isIndex = (
|
|
399
|
-
side
|
|
400
|
-
|
|
416
|
+
const isIndex = (wrapped: TSESTree.Node): boolean => {
|
|
417
|
+
const side = skipTypeWrappers(wrapped);
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
side.type === AST_NODE_TYPES.Identifier &&
|
|
421
|
+
side.name === indexParam.name
|
|
422
|
+
);
|
|
423
|
+
};
|
|
401
424
|
|
|
402
425
|
const [op, other] = isIndex(body.left)
|
|
403
426
|
? ([rawOperator, body.right] as const)
|
|
@@ -421,7 +444,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
421
444
|
return classifyLiteralComparison(op, n);
|
|
422
445
|
}
|
|
423
446
|
|
|
424
|
-
if (!isSimpleReference(receiver))
|
|
447
|
+
if (!isSimpleReference(receiver)) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
425
450
|
|
|
426
451
|
const m = matchLengthMinus(other, receiverText);
|
|
427
452
|
|
|
@@ -435,11 +460,15 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
435
460
|
if (args.length === 1) {
|
|
436
461
|
const [start] = args;
|
|
437
462
|
|
|
438
|
-
if (start === undefined)
|
|
463
|
+
if (start === undefined) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
439
466
|
|
|
440
467
|
const n = asNonNegativeIntegerLiteral(start);
|
|
441
468
|
|
|
442
|
-
if (n !== undefined)
|
|
469
|
+
if (n !== undefined) {
|
|
470
|
+
return normalizeCountedOp('skip', n);
|
|
471
|
+
}
|
|
443
472
|
|
|
444
473
|
const negated = asNegatedPositiveIntegerLiteral(start);
|
|
445
474
|
|
|
@@ -461,7 +490,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
461
490
|
|
|
462
491
|
const n = asNonNegativeIntegerLiteral(end);
|
|
463
492
|
|
|
464
|
-
if (n !== undefined)
|
|
493
|
+
if (n !== undefined) {
|
|
494
|
+
return normalizeCountedOp('take', n);
|
|
495
|
+
}
|
|
465
496
|
|
|
466
497
|
const negated = asNegatedPositiveIntegerLiteral(end);
|
|
467
498
|
|
|
@@ -469,7 +500,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
469
500
|
return normalizeCountedOp('skipLast', negated);
|
|
470
501
|
}
|
|
471
502
|
|
|
472
|
-
if (!isSimpleReference(receiver))
|
|
503
|
+
if (!isSimpleReference(receiver)) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
473
506
|
|
|
474
507
|
const m = matchLengthMinus(end, sourceCode.getText(receiver));
|
|
475
508
|
|
|
@@ -487,12 +520,16 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
487
520
|
if (args.length === 1) {
|
|
488
521
|
const [start] = args;
|
|
489
522
|
|
|
490
|
-
if (start === undefined)
|
|
523
|
+
if (start === undefined) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
491
526
|
|
|
492
527
|
const n = asNonNegativeIntegerLiteral(start);
|
|
493
528
|
|
|
494
529
|
// `xs.toSpliced(N)` removes everything from index N on → take N.
|
|
495
|
-
if (n !== undefined)
|
|
530
|
+
if (n !== undefined) {
|
|
531
|
+
return normalizeCountedOp('take', n);
|
|
532
|
+
}
|
|
496
533
|
|
|
497
534
|
const negated = asNegatedPositiveIntegerLiteral(start);
|
|
498
535
|
|
|
@@ -516,12 +553,16 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
516
553
|
// `xs.toSpliced(0, N)` removes the first N elements → skip N.
|
|
517
554
|
const n = asNonNegativeIntegerLiteral(deleteCount);
|
|
518
555
|
|
|
519
|
-
if (n !== undefined)
|
|
556
|
+
if (n !== undefined) {
|
|
557
|
+
return normalizeCountedOp('skip', n);
|
|
558
|
+
}
|
|
520
559
|
|
|
521
560
|
// `xs.toSpliced(0, xs.length - N)` keeps the last N → takeLast N.
|
|
522
561
|
// (Note: `xs.toSpliced(0, -N)` clamps the delete count to 0 and is a
|
|
523
562
|
// plain copy, so it is deliberately NOT treated as takeLast.)
|
|
524
|
-
if (!isSimpleReference(receiver))
|
|
563
|
+
if (!isSimpleReference(receiver)) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
525
566
|
|
|
526
567
|
const m = matchLengthMinus(deleteCount, receiverText);
|
|
527
568
|
|
|
@@ -626,7 +667,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
626
667
|
element.type !== AST_NODE_TYPES.SpreadElement &&
|
|
627
668
|
node.arguments.length === 1 &&
|
|
628
669
|
arg.type !== AST_NODE_TYPES.SpreadElement &&
|
|
629
|
-
|
|
670
|
+
holdsThroughWrappers(arg, isArrayOrTupleType)
|
|
630
671
|
) {
|
|
631
672
|
report(
|
|
632
673
|
node,
|
|
@@ -649,7 +690,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
649
690
|
|
|
650
691
|
// The receiver must be an array or tuple (not a string / typed array
|
|
651
692
|
// / other object that happens to share the method name).
|
|
652
|
-
if (!
|
|
693
|
+
if (!holdsThroughWrappers(receiver, isArrayOrTupleType)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
653
696
|
|
|
654
697
|
const receiverText = sourceCode.getText(receiver);
|
|
655
698
|
|
|
@@ -657,7 +700,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
657
700
|
case 'slice': {
|
|
658
701
|
const op = classifySliceArgs(node.arguments, receiver);
|
|
659
702
|
|
|
660
|
-
if (op !== undefined)
|
|
703
|
+
if (op !== undefined) {
|
|
704
|
+
report(node, op, toArgText(receiver));
|
|
705
|
+
}
|
|
661
706
|
|
|
662
707
|
return;
|
|
663
708
|
}
|
|
@@ -669,7 +714,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
669
714
|
receiverText,
|
|
670
715
|
);
|
|
671
716
|
|
|
672
|
-
if (op !== undefined)
|
|
717
|
+
if (op !== undefined) {
|
|
718
|
+
report(node, op, toArgText(receiver));
|
|
719
|
+
}
|
|
673
720
|
|
|
674
721
|
return;
|
|
675
722
|
}
|
|
@@ -683,7 +730,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
683
730
|
|
|
684
731
|
const op = classifyFilterCallback(callback, receiver, receiverText);
|
|
685
732
|
|
|
686
|
-
if (op !== undefined)
|
|
733
|
+
if (op !== undefined) {
|
|
734
|
+
report(node, op, toArgText(receiver));
|
|
735
|
+
}
|
|
687
736
|
|
|
688
737
|
return;
|
|
689
738
|
}
|
|
@@ -721,7 +770,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
721
770
|
|
|
722
771
|
// `xs.concat(item)` appends only when `item` can never be an
|
|
723
772
|
// array (otherwise `concat` would flatten it).
|
|
724
|
-
if (
|
|
773
|
+
if (holdsThroughWrappers(arg, isDefinitelyNotArrayType)) {
|
|
725
774
|
report(
|
|
726
775
|
node,
|
|
727
776
|
{ fn: 'toPushed', itemText: toArgText(arg) },
|
|
@@ -735,7 +784,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
735
784
|
ArrayExpression: (node) => {
|
|
736
785
|
// `[item, ...xs]` → `Arr.toUnshifted(xs, item)`,
|
|
737
786
|
// `[...xs, item]` → `Arr.toPushed(xs, item)`.
|
|
738
|
-
if (node.elements.length !== 2)
|
|
787
|
+
if (node.elements.length !== 2) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
739
790
|
|
|
740
791
|
const [first, second] = node.elements;
|
|
741
792
|
|
|
@@ -760,11 +811,15 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
760
811
|
// `spread`, `item`, and `fn` are set together (all from the same
|
|
761
812
|
// tuple), so checking `spread` also guarantees the others are
|
|
762
813
|
// defined.
|
|
763
|
-
if (spread === undefined)
|
|
814
|
+
if (spread === undefined) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
764
817
|
|
|
765
818
|
// Spreading a non-array iterable (`[v, ...someSet]`) is a genuine
|
|
766
819
|
// conversion, not an element addition to an array.
|
|
767
|
-
if (!
|
|
820
|
+
if (!holdsThroughWrappers(spread.argument, isArrayOrTupleType)) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
768
823
|
|
|
769
824
|
report(
|
|
770
825
|
node,
|
|
@@ -586,3 +586,95 @@ describe('prefer-canonical-array-slicing', () => {
|
|
|
586
586
|
],
|
|
587
587
|
});
|
|
588
588
|
});
|
|
589
|
+
|
|
590
|
+
describe('prefer-canonical-array-slicing through type wrappers', () => {
|
|
591
|
+
tester.run('prefer-canonical-array-slicing', preferCanonicalArraySlicing, {
|
|
592
|
+
valid: [
|
|
593
|
+
{
|
|
594
|
+
name: 'a concat argument cast away from an array may still be flattened',
|
|
595
|
+
code: dedent`
|
|
596
|
+
declare const xs: readonly number[];
|
|
597
|
+
declare const v: number | readonly number[];
|
|
598
|
+
const ys = xs.concat(v as number);
|
|
599
|
+
`,
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'a spread Set cast to an array is still a conversion',
|
|
603
|
+
code: dedent`
|
|
604
|
+
declare const s: ReadonlySet<number>;
|
|
605
|
+
const ys = [...(s as unknown as readonly number[]), 1];
|
|
606
|
+
`,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'a receiver cast to an array is still not one',
|
|
610
|
+
code: dedent`
|
|
611
|
+
declare const s: string;
|
|
612
|
+
const ys = (s as unknown as readonly string[]).slice(1);
|
|
613
|
+
`,
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'a concat argument cast to an array is still not one',
|
|
617
|
+
code: dedent`
|
|
618
|
+
declare const s: ReadonlySet<number>;
|
|
619
|
+
const ys = [1].concat(s as unknown as readonly number[]);
|
|
620
|
+
`,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
invalid: [
|
|
624
|
+
{
|
|
625
|
+
name: 'a count wrapped in `as` or `satisfies`',
|
|
626
|
+
code: dedent`
|
|
627
|
+
declare const xs: readonly number[];
|
|
628
|
+
const a = xs.slice(1 as number);
|
|
629
|
+
const b = xs.slice(0 satisfies number, -1 as number);
|
|
630
|
+
const c = xs.toSpliced(2 satisfies number);
|
|
631
|
+
`,
|
|
632
|
+
output: dedent`
|
|
633
|
+
import { Arr } from 'ts-data-forge';
|
|
634
|
+
declare const xs: readonly number[];
|
|
635
|
+
const a = Arr.tail(xs);
|
|
636
|
+
const b = Arr.butLast(xs);
|
|
637
|
+
const c = Arr.take(xs, 2);
|
|
638
|
+
`,
|
|
639
|
+
errors: [
|
|
640
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
641
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
642
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: 'a filter comparison on a wrapped index',
|
|
647
|
+
code: dedent`
|
|
648
|
+
declare const xs: readonly number[];
|
|
649
|
+
const ys = xs.filter((_, i) => (i satisfies number) >= 1);
|
|
650
|
+
`,
|
|
651
|
+
output: dedent`
|
|
652
|
+
import { Arr } from 'ts-data-forge';
|
|
653
|
+
declare const xs: readonly number[];
|
|
654
|
+
const ys = Arr.tail(xs);
|
|
655
|
+
`,
|
|
656
|
+
errors: [{ messageId: 'preferCanonicalArraySlicing' }],
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: 'a wrapped receiver and item keep their wrappers',
|
|
660
|
+
code: dedent`
|
|
661
|
+
declare const xs: readonly number[];
|
|
662
|
+
declare const v: number | undefined;
|
|
663
|
+
const ys = (xs satisfies readonly number[]).slice(1);
|
|
664
|
+
const zs = [...xs!, v!];
|
|
665
|
+
`,
|
|
666
|
+
output: dedent`
|
|
667
|
+
import { Arr } from 'ts-data-forge';
|
|
668
|
+
declare const xs: readonly number[];
|
|
669
|
+
declare const v: number | undefined;
|
|
670
|
+
const ys = Arr.tail(xs satisfies readonly number[]);
|
|
671
|
+
const zs = Arr.toPushed(xs!, v!);
|
|
672
|
+
`,
|
|
673
|
+
errors: [
|
|
674
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
675
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
});
|
|
680
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AST_NODE_TYPES,
|
|
3
|
+
ASTUtils,
|
|
3
4
|
type TSESLint,
|
|
4
5
|
type TSESTree,
|
|
5
6
|
} from '@typescript-eslint/utils';
|
|
7
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
6
8
|
import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
|
|
7
9
|
|
|
8
10
|
type Options = readonly [];
|
|
@@ -145,20 +147,26 @@ export const preferCanonicalLengthCast: TSESLint.RuleModule<
|
|
|
145
147
|
'Arr',
|
|
146
148
|
);
|
|
147
149
|
|
|
148
|
-
if (arrLocalName === undefined)
|
|
150
|
+
if (arrLocalName === undefined) {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
149
153
|
|
|
150
154
|
return {
|
|
151
155
|
CallExpression: (node) => {
|
|
152
156
|
const castName = getCastName(node, arrLocalName);
|
|
153
157
|
|
|
154
|
-
if (castName === undefined)
|
|
158
|
+
if (castName === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
155
161
|
|
|
156
162
|
const rewrite = CAST_REWRITES.find(
|
|
157
163
|
(candidate) =>
|
|
158
164
|
candidate.cast === castName && matchesBounds(node, candidate),
|
|
159
165
|
);
|
|
160
166
|
|
|
161
|
-
if (rewrite === undefined)
|
|
167
|
+
if (rewrite === undefined) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
162
170
|
|
|
163
171
|
const boundsText = node.arguments
|
|
164
172
|
.slice(0, rewrite.bounds.length)
|
|
@@ -179,7 +187,7 @@ export const preferCanonicalLengthCast: TSESLint.RuleModule<
|
|
|
179
187
|
node.callee,
|
|
180
188
|
`${arrLocalName}.${rewrite.replacement}`,
|
|
181
189
|
),
|
|
182
|
-
...dropBoundsFix(fixer, node, rewrite),
|
|
190
|
+
...dropBoundsFix(fixer, sourceCode, node, rewrite),
|
|
183
191
|
],
|
|
184
192
|
});
|
|
185
193
|
},
|
|
@@ -238,7 +246,9 @@ const matchesBounds = (
|
|
|
238
246
|
return rewrite.bounds.every((matcher, index) => {
|
|
239
247
|
const value = bounds[index];
|
|
240
248
|
|
|
241
|
-
if (value === undefined)
|
|
249
|
+
if (value === undefined) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
242
252
|
|
|
243
253
|
switch (matcher) {
|
|
244
254
|
case 'anyLength':
|
|
@@ -255,11 +265,16 @@ const matchesBounds = (
|
|
|
255
265
|
|
|
256
266
|
/**
|
|
257
267
|
* Removes the length arguments the rewrite drops. Each removal runs from the
|
|
258
|
-
*
|
|
259
|
-
*
|
|
268
|
+
* first token after the delimiter before the dropped argument (the call's `(`
|
|
269
|
+
* or the previous comma) up to the first token after the comma that follows
|
|
270
|
+
* it. Parentheses the source put around the dropped argument, the comma,
|
|
271
|
+
* whitespace and any comments in between go with it; parentheses around the
|
|
272
|
+
* next argument stay. The ranges never overlap.
|
|
260
273
|
*/
|
|
261
274
|
const dropBoundsFix = (
|
|
262
275
|
fixer: TSESLint.RuleFixer,
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
277
|
+
sourceCode: TSESLint.SourceCode,
|
|
263
278
|
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
264
279
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
265
280
|
node: TSESTree.CallExpression,
|
|
@@ -275,23 +290,46 @@ const dropBoundsFix = (
|
|
|
275
290
|
// "not of this type".
|
|
276
291
|
const dropped = node.arguments[index];
|
|
277
292
|
|
|
278
|
-
const
|
|
293
|
+
const previous = node.arguments[index - 1];
|
|
294
|
+
|
|
295
|
+
if (dropped === undefined || rewrite.keep.includes(index)) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const delimiterBefore =
|
|
300
|
+
previous === undefined
|
|
301
|
+
? sourceCode.getTokenAfter(node.callee, ASTUtils.isOpeningParenToken)
|
|
302
|
+
: sourceCode.getTokenAfter(previous, ASTUtils.isCommaToken);
|
|
279
303
|
|
|
280
|
-
|
|
304
|
+
const start =
|
|
305
|
+
delimiterBefore === null
|
|
306
|
+
? null
|
|
307
|
+
: sourceCode.getTokenAfter(delimiterBefore);
|
|
281
308
|
|
|
282
|
-
|
|
309
|
+
const commaAfter = sourceCode.getTokenAfter(dropped, ASTUtils.isCommaToken);
|
|
310
|
+
|
|
311
|
+
const end =
|
|
312
|
+
commaAfter === null ? null : sourceCode.getTokenAfter(commaAfter);
|
|
313
|
+
|
|
314
|
+
return start === null || end === null
|
|
283
315
|
? []
|
|
284
|
-
: [fixer.removeRange([
|
|
316
|
+
: [fixer.removeRange([start.range[0], end.range[0]])];
|
|
285
317
|
});
|
|
286
318
|
|
|
287
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* A non-negative integer literal's value, or `undefined` for anything else.
|
|
321
|
+
* Read through type wrappers: `1 as const` is still the bound `1`.
|
|
322
|
+
*/
|
|
288
323
|
const getLengthLiteral = (
|
|
289
324
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
290
|
-
|
|
291
|
-
): number | undefined =>
|
|
292
|
-
node
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
325
|
+
wrapped: TSESTree.Node,
|
|
326
|
+
): number | undefined => {
|
|
327
|
+
const node = skipTypeWrappers(wrapped);
|
|
328
|
+
|
|
329
|
+
return node.type === AST_NODE_TYPES.Literal &&
|
|
330
|
+
typeof node.value === 'number' &&
|
|
331
|
+
Number.isSafeInteger(node.value) &&
|
|
332
|
+
node.value >= 0
|
|
296
333
|
? node.value
|
|
297
334
|
: undefined;
|
|
335
|
+
};
|
|
@@ -303,3 +303,114 @@ describe('prefer-canonical-length-cast', () => {
|
|
|
303
303
|
],
|
|
304
304
|
});
|
|
305
305
|
});
|
|
306
|
+
|
|
307
|
+
describe('prefer-canonical-length-cast through type wrappers', () => {
|
|
308
|
+
tester.run('prefer-canonical-length-cast', preferCanonicalLengthCast, {
|
|
309
|
+
valid: [
|
|
310
|
+
{
|
|
311
|
+
name: 'a wrapped bound other than the degenerate one',
|
|
312
|
+
code: dedent`
|
|
313
|
+
import { Arr } from 'ts-data-forge';
|
|
314
|
+
|
|
315
|
+
declare const xs: readonly number[];
|
|
316
|
+
const ys = Arr.asMinLengthArray(2 as const, xs);
|
|
317
|
+
`,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
invalid: [
|
|
321
|
+
{
|
|
322
|
+
name: 'a bound wrapped in `as const`',
|
|
323
|
+
code: dedent`
|
|
324
|
+
import { Arr } from 'ts-data-forge';
|
|
325
|
+
|
|
326
|
+
declare const xs: readonly number[];
|
|
327
|
+
const ys = Arr.asMinLengthArray(1 as const, xs);
|
|
328
|
+
`,
|
|
329
|
+
output: dedent`
|
|
330
|
+
import { Arr } from 'ts-data-forge';
|
|
331
|
+
|
|
332
|
+
declare const xs: readonly number[];
|
|
333
|
+
const ys = Arr.asNonEmptyArray(xs);
|
|
334
|
+
`,
|
|
335
|
+
errors: [{ messageId: 'useCanonicalCast' }],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'bounds wrapped in `satisfies`, the kept one keeping its wrapper',
|
|
339
|
+
code: dedent`
|
|
340
|
+
import { Arr } from 'ts-data-forge';
|
|
341
|
+
|
|
342
|
+
declare const xs: readonly number[];
|
|
343
|
+
const ys = Arr.asBoundedLengthTuple(3 satisfies number, 3 as const, xs!);
|
|
344
|
+
`,
|
|
345
|
+
output: dedent`
|
|
346
|
+
import { Arr } from 'ts-data-forge';
|
|
347
|
+
|
|
348
|
+
declare const xs: readonly number[];
|
|
349
|
+
const ys = Arr.asFixedLengthTuple(3 satisfies number, xs!);
|
|
350
|
+
`,
|
|
351
|
+
errors: [{ messageId: 'useCanonicalCast' }],
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('prefer-canonical-length-cast with parenthesized operands', () => {
|
|
358
|
+
tester.run('prefer-canonical-length-cast', preferCanonicalLengthCast, {
|
|
359
|
+
valid: [],
|
|
360
|
+
invalid: [
|
|
361
|
+
{
|
|
362
|
+
name: 'a parenthesized array after the dropped bound',
|
|
363
|
+
code: dedent`
|
|
364
|
+
import { Arr } from 'ts-data-forge';
|
|
365
|
+
|
|
366
|
+
declare const xs: readonly number[];
|
|
367
|
+
const ys = Arr.asMinLengthArray(1, (xs));
|
|
368
|
+
`,
|
|
369
|
+
output: dedent`
|
|
370
|
+
import { Arr } from 'ts-data-forge';
|
|
371
|
+
|
|
372
|
+
declare const xs: readonly number[];
|
|
373
|
+
const ys = Arr.asNonEmptyArray((xs));
|
|
374
|
+
`,
|
|
375
|
+
errors: [{ messageId: 'useCanonicalCast' }],
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'a parenthesized dropped bound',
|
|
379
|
+
code: dedent`
|
|
380
|
+
import { Arr } from 'ts-data-forge';
|
|
381
|
+
|
|
382
|
+
declare const xs: readonly number[];
|
|
383
|
+
const ys = Arr.asMinLengthArray((1), xs);
|
|
384
|
+
`,
|
|
385
|
+
output: dedent`
|
|
386
|
+
import { Arr } from 'ts-data-forge';
|
|
387
|
+
|
|
388
|
+
declare const xs: readonly number[];
|
|
389
|
+
const ys = Arr.asNonEmptyArray(xs);
|
|
390
|
+
`,
|
|
391
|
+
errors: [{ messageId: 'useCanonicalCast' }],
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'a parenthesized second bound of a bounded cast',
|
|
395
|
+
code: dedent`
|
|
396
|
+
import { Arr } from 'ts-data-forge';
|
|
397
|
+
|
|
398
|
+
declare const xs: readonly number[];
|
|
399
|
+
const ys = Arr.asBoundedLengthArray(2, ((2)), xs);
|
|
400
|
+
const zs = Arr.asBoundedLengthArray((0), (0), (xs));
|
|
401
|
+
`,
|
|
402
|
+
output: dedent`
|
|
403
|
+
import { Arr } from 'ts-data-forge';
|
|
404
|
+
|
|
405
|
+
declare const xs: readonly number[];
|
|
406
|
+
const ys = Arr.asFixedLengthArray(2, xs);
|
|
407
|
+
const zs = Arr.asEmptyArray((xs));
|
|
408
|
+
`,
|
|
409
|
+
errors: [
|
|
410
|
+
{ messageId: 'useCanonicalCast' },
|
|
411
|
+
{ messageId: 'useCanonicalCast' },
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { MutableRecord, ReadonlyRecord } from 'ts-type-forge';
|
|
7
|
+
import { skipTypeWrappers, toArgumentText } from './ast-utils.mjs';
|
|
7
8
|
import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
|
|
8
9
|
import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
|
|
9
10
|
import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
|
|
@@ -124,7 +125,9 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
124
125
|
rule.create(context),
|
|
125
126
|
);
|
|
126
127
|
|
|
127
|
-
if (arrLocalName === undefined)
|
|
128
|
+
if (arrLocalName === undefined) {
|
|
129
|
+
return mergeVisitors(comparisonVisitors);
|
|
130
|
+
}
|
|
128
131
|
|
|
129
132
|
return mergeVisitors([
|
|
130
133
|
...comparisonVisitors,
|
|
@@ -132,7 +135,9 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
132
135
|
CallExpression: (node) => {
|
|
133
136
|
const guardName = getGuardName(node, arrLocalName);
|
|
134
137
|
|
|
135
|
-
if (guardName === undefined)
|
|
138
|
+
if (guardName === undefined) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
136
141
|
|
|
137
142
|
const rewrite = GUARD_REWRITES.find(
|
|
138
143
|
(entry) => entry.guard === guardName,
|
|
@@ -145,9 +150,11 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
145
150
|
// The array is the last argument; the bounds precede it.
|
|
146
151
|
const array = node.arguments[rewrite.bounds.length];
|
|
147
152
|
|
|
148
|
-
if (array === undefined)
|
|
153
|
+
if (array === undefined) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
149
156
|
|
|
150
|
-
const arrayText =
|
|
157
|
+
const arrayText = toArgumentText(array, sourceCode);
|
|
151
158
|
|
|
152
159
|
context.report({
|
|
153
160
|
node,
|
|
@@ -185,7 +192,9 @@ const mergeVisitors = (
|
|
|
185
192
|
|
|
186
193
|
for (const visitor of visitors) {
|
|
187
194
|
for (const [selector, handler] of Object.entries(visitor)) {
|
|
188
|
-
if (typeof handler !== 'function')
|
|
195
|
+
if (typeof handler !== 'function') {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
189
198
|
|
|
190
199
|
const mut_handlers = mut_merged[selector] ?? [];
|
|
191
200
|
|
|
@@ -226,7 +235,8 @@ const getGuardName = (
|
|
|
226
235
|
|
|
227
236
|
/**
|
|
228
237
|
* Whether the call is `guard(...bounds, array)` with every bound written as the
|
|
229
|
-
* exact numeric literal the rewrite requires
|
|
238
|
+
* exact numeric literal the rewrite requires, under any type wrapper
|
|
239
|
+
* (`0 as const` is still the bound 0).
|
|
230
240
|
*
|
|
231
241
|
* The arity check also excludes the curried form (`guard(...bounds)`): the
|
|
232
242
|
* degenerate guards take only the array, so there is nothing to rewrite it to.
|
|
@@ -242,10 +252,14 @@ const matchesBounds = (
|
|
|
242
252
|
return false;
|
|
243
253
|
}
|
|
244
254
|
|
|
245
|
-
return args.every(
|
|
246
|
-
(arg
|
|
255
|
+
return args.every((arg, index) => {
|
|
256
|
+
const bound = skipTypeWrappers(arg);
|
|
257
|
+
|
|
258
|
+
return (
|
|
247
259
|
arg.type !== AST_NODE_TYPES.SpreadElement &&
|
|
248
260
|
(index === bounds.length ||
|
|
249
|
-
(
|
|
250
|
-
|
|
261
|
+
(bound.type === AST_NODE_TYPES.Literal &&
|
|
262
|
+
bound.value === bounds[index]))
|
|
263
|
+
);
|
|
264
|
+
});
|
|
251
265
|
};
|