eslint-plugin-ts-data-forge 0.8.2 → 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 +119 -8
- package/dist/rules/ast-utils.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 +22 -11
- 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 +125 -79
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +11 -4
- 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 +5 -19
- 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 +11 -27
- 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 +9 -22
- 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 +9 -22
- 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 +7 -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 +33 -13
- 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 +51 -37
- 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 +33 -10
- 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 +18 -12
- 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 +29 -14
- 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 +34 -13
- 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 +10 -4
- 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 +25 -3
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +28 -10
- 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 +8 -3
- 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 +18 -7
- 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 +15 -8
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +13 -7
- 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 +3 -3
- 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 +156 -13
- package/src/rules/import-utils.mts +35 -15
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/no-unnecessary-type-guard.mts +192 -124
- package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
- package/src/rules/prefer-arr-is-array.mts +11 -6
- package/src/rules/prefer-arr-is-array.test.mts +40 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +13 -25
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +18 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +15 -26
- package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +15 -26
- package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -25
- package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
- package/src/rules/prefer-arr-scan.mts +55 -19
- package/src/rules/prefer-arr-scan.test.mts +149 -0
- package/src/rules/prefer-arr-sum.mts +78 -47
- package/src/rules/prefer-arr-sum.test.mts +97 -0
- package/src/rules/prefer-arr-uniq.mts +46 -12
- package/src/rules/prefer-arr-uniq.test.mts +71 -0
- package/src/rules/prefer-as-int.mts +24 -16
- package/src/rules/prefer-as-int.test.mts +51 -0
- package/src/rules/prefer-canonical-array-slicing.mts +38 -15
- package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
- package/src/rules/prefer-canonical-length-cast.mts +45 -15
- package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
- package/src/rules/prefer-canonical-length-guard.mts +13 -7
- package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +33 -6
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
- package/src/rules/prefer-is-non-null-object.mts +31 -11
- package/src/rules/prefer-is-non-null-object.test.mts +42 -0
- package/src/rules/prefer-is-record-and-has-key.mts +10 -5
- package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
- package/src/rules/prefer-num-safe-parse-float.mts +24 -11
- package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
- package/src/rules/prefer-num-safe-parse-int.mts +25 -11
- package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +1 -1
- package/src/rules/prefer-range-for-loop.mts +18 -8
- 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)
|
|
@@ -644,7 +667,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
644
667
|
element.type !== AST_NODE_TYPES.SpreadElement &&
|
|
645
668
|
node.arguments.length === 1 &&
|
|
646
669
|
arg.type !== AST_NODE_TYPES.SpreadElement &&
|
|
647
|
-
|
|
670
|
+
holdsThroughWrappers(arg, isArrayOrTupleType)
|
|
648
671
|
) {
|
|
649
672
|
report(
|
|
650
673
|
node,
|
|
@@ -667,7 +690,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
667
690
|
|
|
668
691
|
// The receiver must be an array or tuple (not a string / typed array
|
|
669
692
|
// / other object that happens to share the method name).
|
|
670
|
-
if (!
|
|
693
|
+
if (!holdsThroughWrappers(receiver, isArrayOrTupleType)) {
|
|
671
694
|
return;
|
|
672
695
|
}
|
|
673
696
|
|
|
@@ -747,7 +770,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
747
770
|
|
|
748
771
|
// `xs.concat(item)` appends only when `item` can never be an
|
|
749
772
|
// array (otherwise `concat` would flatten it).
|
|
750
|
-
if (
|
|
773
|
+
if (holdsThroughWrappers(arg, isDefinitelyNotArrayType)) {
|
|
751
774
|
report(
|
|
752
775
|
node,
|
|
753
776
|
{ fn: 'toPushed', itemText: toArgText(arg) },
|
|
@@ -794,7 +817,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
794
817
|
|
|
795
818
|
// Spreading a non-array iterable (`[v, ...someSet]`) is a genuine
|
|
796
819
|
// conversion, not an element addition to an array.
|
|
797
|
-
if (!
|
|
820
|
+
if (!holdsThroughWrappers(spread.argument, isArrayOrTupleType)) {
|
|
798
821
|
return;
|
|
799
822
|
}
|
|
800
823
|
|
|
@@ -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 [];
|
|
@@ -185,7 +187,7 @@ export const preferCanonicalLengthCast: TSESLint.RuleModule<
|
|
|
185
187
|
node.callee,
|
|
186
188
|
`${arrLocalName}.${rewrite.replacement}`,
|
|
187
189
|
),
|
|
188
|
-
...dropBoundsFix(fixer, node, rewrite),
|
|
190
|
+
...dropBoundsFix(fixer, sourceCode, node, rewrite),
|
|
189
191
|
],
|
|
190
192
|
});
|
|
191
193
|
},
|
|
@@ -263,11 +265,16 @@ const matchesBounds = (
|
|
|
263
265
|
|
|
264
266
|
/**
|
|
265
267
|
* Removes the length arguments the rewrite drops. Each removal runs from the
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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.
|
|
268
273
|
*/
|
|
269
274
|
const dropBoundsFix = (
|
|
270
275
|
fixer: TSESLint.RuleFixer,
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
277
|
+
sourceCode: TSESLint.SourceCode,
|
|
271
278
|
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
272
279
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
273
280
|
node: TSESTree.CallExpression,
|
|
@@ -283,23 +290,46 @@ const dropBoundsFix = (
|
|
|
283
290
|
// "not of this type".
|
|
284
291
|
const dropped = node.arguments[index];
|
|
285
292
|
|
|
286
|
-
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);
|
|
303
|
+
|
|
304
|
+
const start =
|
|
305
|
+
delimiterBefore === null
|
|
306
|
+
? null
|
|
307
|
+
: sourceCode.getTokenAfter(delimiterBefore);
|
|
287
308
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
291
315
|
? []
|
|
292
|
-
: [fixer.removeRange([
|
|
316
|
+
: [fixer.removeRange([start.range[0], end.range[0]])];
|
|
293
317
|
});
|
|
294
318
|
|
|
295
|
-
/**
|
|
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
|
+
*/
|
|
296
323
|
const getLengthLiteral = (
|
|
297
324
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
298
|
-
|
|
299
|
-
): number | undefined =>
|
|
300
|
-
node
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
304
333
|
? node.value
|
|
305
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';
|
|
@@ -153,7 +154,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
const arrayText =
|
|
157
|
+
const arrayText = toArgumentText(array, sourceCode);
|
|
157
158
|
|
|
158
159
|
context.report({
|
|
159
160
|
node,
|
|
@@ -234,7 +235,8 @@ const getGuardName = (
|
|
|
234
235
|
|
|
235
236
|
/**
|
|
236
237
|
* Whether the call is `guard(...bounds, array)` with every bound written as the
|
|
237
|
-
* 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).
|
|
238
240
|
*
|
|
239
241
|
* The arity check also excludes the curried form (`guard(...bounds)`): the
|
|
240
242
|
* degenerate guards take only the array, so there is nothing to rewrite it to.
|
|
@@ -250,10 +252,14 @@ const matchesBounds = (
|
|
|
250
252
|
return false;
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
return args.every(
|
|
254
|
-
(arg
|
|
255
|
+
return args.every((arg, index) => {
|
|
256
|
+
const bound = skipTypeWrappers(arg);
|
|
257
|
+
|
|
258
|
+
return (
|
|
255
259
|
arg.type !== AST_NODE_TYPES.SpreadElement &&
|
|
256
260
|
(index === bounds.length ||
|
|
257
|
-
(
|
|
258
|
-
|
|
261
|
+
(bound.type === AST_NODE_TYPES.Literal &&
|
|
262
|
+
bound.value === bounds[index]))
|
|
263
|
+
);
|
|
264
|
+
});
|
|
259
265
|
};
|
|
@@ -305,3 +305,102 @@ describe('prefer-canonical-length-guard', () => {
|
|
|
305
305
|
],
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
+
|
|
309
|
+
describe('prefer-canonical-length-guard through type wrappers', () => {
|
|
310
|
+
tester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
311
|
+
valid: [
|
|
312
|
+
{
|
|
313
|
+
name: 'a wrapped bound other than the degenerate one',
|
|
314
|
+
code: dedent`
|
|
315
|
+
import { Arr } from 'ts-data-forge';
|
|
316
|
+
|
|
317
|
+
declare const xs: readonly number[];
|
|
318
|
+
const ok = Arr.isFixedLengthArray(1 as const, xs);
|
|
319
|
+
`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
invalid: [
|
|
323
|
+
{
|
|
324
|
+
name: 'a bound wrapped in `as const` or `satisfies`',
|
|
325
|
+
code: dedent`
|
|
326
|
+
import { Arr } from 'ts-data-forge';
|
|
327
|
+
|
|
328
|
+
declare const xs: readonly number[];
|
|
329
|
+
const a = Arr.isFixedLengthArray(0 as const, xs);
|
|
330
|
+
const b = Arr.isBoundedLengthTuple(0 satisfies number, 0, xs!);
|
|
331
|
+
`,
|
|
332
|
+
output: dedent`
|
|
333
|
+
import { Arr } from 'ts-data-forge';
|
|
334
|
+
|
|
335
|
+
declare const xs: readonly number[];
|
|
336
|
+
const a = Arr.isEmpty(xs);
|
|
337
|
+
const b = Arr.isEmptyTuple(xs!);
|
|
338
|
+
`,
|
|
339
|
+
errors: [
|
|
340
|
+
{ messageId: 'useCanonicalGuard' },
|
|
341
|
+
{ messageId: 'useCanonicalGuard' },
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
typedTester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
348
|
+
valid: [
|
|
349
|
+
{
|
|
350
|
+
name: 'a computed index named `length` is not the length',
|
|
351
|
+
code: dedent`
|
|
352
|
+
declare const xs: readonly number[];
|
|
353
|
+
declare const length: number;
|
|
354
|
+
const ok = xs[length] > 0;
|
|
355
|
+
`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
invalid: [
|
|
359
|
+
{
|
|
360
|
+
name: 'a folded-in comparison on a wrapped length',
|
|
361
|
+
code: dedent`
|
|
362
|
+
import { Arr } from 'ts-data-forge';
|
|
363
|
+
declare const xs: readonly number[];
|
|
364
|
+
const a = (xs.length satisfies number) > 0;
|
|
365
|
+
const b = xs.length >= 1 && (xs as readonly number[]).length <= 3;
|
|
366
|
+
`,
|
|
367
|
+
output: dedent`
|
|
368
|
+
import { Arr } from 'ts-data-forge';
|
|
369
|
+
declare const xs: readonly number[];
|
|
370
|
+
const a = Arr.isNonEmpty(xs);
|
|
371
|
+
const b = Arr.isBoundedLengthArray(1, 3, xs);
|
|
372
|
+
`,
|
|
373
|
+
errors: [
|
|
374
|
+
{ messageId: 'useIsNonEmpty' },
|
|
375
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('prefer-canonical-length-guard with parenthesized operands', () => {
|
|
383
|
+
tester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
384
|
+
valid: [],
|
|
385
|
+
invalid: [
|
|
386
|
+
{
|
|
387
|
+
name: 'a sequence array keeps its parentheses',
|
|
388
|
+
code: dedent`
|
|
389
|
+
import { Arr } from 'ts-data-forge';
|
|
390
|
+
|
|
391
|
+
declare const xs: readonly number[];
|
|
392
|
+
declare const log: () => void;
|
|
393
|
+
const ok = Arr.isFixedLengthArray(0, (log(), xs));
|
|
394
|
+
`,
|
|
395
|
+
output: dedent`
|
|
396
|
+
import { Arr } from 'ts-data-forge';
|
|
397
|
+
|
|
398
|
+
declare const xs: readonly number[];
|
|
399
|
+
declare const log: () => void;
|
|
400
|
+
const ok = Arr.isEmpty((log(), xs));
|
|
401
|
+
`,
|
|
402
|
+
errors: [{ messageId: 'useCanonicalGuard' }],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
|
|
7
7
|
import { buildCalleeResolver, getTsDataForgeImport } from './import-utils.mjs';
|
|
8
8
|
|
|
9
9
|
type Options = readonly [];
|
|
@@ -29,7 +29,11 @@ const COMPARISON_GUARDS: ReadonlyRecord<string, ComparisonSpec> = {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Argument expression kinds whose precedence is lower than equality, so they
|
|
32
|
-
* must be parenthesized when placed on the left of `===` / `!==`.
|
|
32
|
+
* must be parenthesized when placed on the left of `===` / `!==`. A node's text
|
|
33
|
+
* never includes the parentheses the source wrote around it, so this is decided
|
|
34
|
+
* on the kind alone. `as` / `satisfies` and a nested equality would parse as
|
|
35
|
+
* intended, but are parenthesized so that the type or the grouping is not read
|
|
36
|
+
* as part of the comparison.
|
|
33
37
|
*/
|
|
34
38
|
const ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([
|
|
35
39
|
AST_NODE_TYPES.LogicalExpression,
|
|
@@ -38,11 +42,33 @@ const ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([
|
|
|
38
42
|
AST_NODE_TYPES.SequenceExpression,
|
|
39
43
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
40
44
|
AST_NODE_TYPES.YieldExpression,
|
|
45
|
+
AST_NODE_TYPES.TSAsExpression,
|
|
46
|
+
AST_NODE_TYPES.TSSatisfiesExpression,
|
|
41
47
|
]);
|
|
42
48
|
|
|
49
|
+
/** Binary operators that bind no tighter than `===` / `!==`. */
|
|
50
|
+
const LOOSE_BINARY_OPERATORS: ReadonlySet<string> = new Set([
|
|
51
|
+
'|',
|
|
52
|
+
'^',
|
|
53
|
+
'&',
|
|
54
|
+
'==',
|
|
55
|
+
'!=',
|
|
56
|
+
'===',
|
|
57
|
+
'!==',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const argumentNeedsParens = (
|
|
61
|
+
argument: DeepReadonly<TSESTree.Expression>,
|
|
62
|
+
): boolean =>
|
|
63
|
+
ARG_NEEDS_PARENS.has(argument.type) ||
|
|
64
|
+
(argument.type === AST_NODE_TYPES.BinaryExpression &&
|
|
65
|
+
LOOSE_BINARY_OPERATORS.has(argument.operator));
|
|
66
|
+
|
|
43
67
|
/**
|
|
44
68
|
* Returns `true` when the comparison must be wrapped in parentheses to preserve
|
|
45
|
-
* the original grouping given the call expression's parent context.
|
|
69
|
+
* the original grouping given the call expression's parent context. A type
|
|
70
|
+
* wrapper around the call is one such context: `x === null satisfies boolean`
|
|
71
|
+
* would apply the wrapper to `null`.
|
|
46
72
|
*/
|
|
47
73
|
const needsWrappingInParent = (
|
|
48
74
|
node: DeepReadonly<TSESTree.CallExpression>,
|
|
@@ -56,6 +82,9 @@ const needsWrappingInParent = (
|
|
|
56
82
|
case AST_NODE_TYPES.AwaitExpression:
|
|
57
83
|
case AST_NODE_TYPES.BinaryExpression:
|
|
58
84
|
case AST_NODE_TYPES.TSNonNullExpression:
|
|
85
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
86
|
+
case AST_NODE_TYPES.TSSatisfiesExpression:
|
|
87
|
+
case AST_NODE_TYPES.TSTypeAssertion:
|
|
59
88
|
return true;
|
|
60
89
|
|
|
61
90
|
case AST_NODE_TYPES.MemberExpression:
|
|
@@ -124,9 +153,7 @@ export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
|
|
|
124
153
|
|
|
125
154
|
const argRaw = sourceCode.getText(argument);
|
|
126
155
|
|
|
127
|
-
const argText =
|
|
128
|
-
? `(${argRaw})`
|
|
129
|
-
: argRaw;
|
|
156
|
+
const argText = argumentNeedsParens(argument) ? `(${argRaw})` : argRaw;
|
|
130
157
|
|
|
131
158
|
const comparison = `${argText} ${spec.op} ${spec.literal}`;
|
|
132
159
|
|