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
|
@@ -460,3 +460,103 @@ describe('no-unnecessary-type-guard', () => {
|
|
|
460
460
|
],
|
|
461
461
|
});
|
|
462
462
|
});
|
|
463
|
+
|
|
464
|
+
describe('no-unnecessary-type-guard through type wrappers', () => {
|
|
465
|
+
tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
|
|
466
|
+
valid: [
|
|
467
|
+
{
|
|
468
|
+
name: 'isNullish on a value cast away from null',
|
|
469
|
+
code: dedent`
|
|
470
|
+
import { isNullish } from 'ts-data-forge';
|
|
471
|
+
declare const x: string | null | undefined;
|
|
472
|
+
const y = isNullish(x as string | undefined);
|
|
473
|
+
`,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'isNonNullish on a value cast away from null',
|
|
477
|
+
code: dedent`
|
|
478
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
479
|
+
declare const x: string | null | undefined;
|
|
480
|
+
const y = isNonNullish(x as string | undefined);
|
|
481
|
+
`,
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'isNonEmptyString on a string cast to NonEmptyString',
|
|
485
|
+
code: dedent`
|
|
486
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
487
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
488
|
+
declare const x: string | null | undefined;
|
|
489
|
+
const y = isNonEmptyString(x as NonEmptyString | null | undefined);
|
|
490
|
+
`,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'isNull on a value whose `!` does not remove the null',
|
|
494
|
+
code: dedent`
|
|
495
|
+
import { isNull } from 'ts-data-forge';
|
|
496
|
+
declare const x: string | null;
|
|
497
|
+
const y = isNull(x!);
|
|
498
|
+
`,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'isNull on a value widened to include null: the `as` is the author saying it may be null',
|
|
502
|
+
code: dedent`
|
|
503
|
+
import { isNull } from 'ts-data-forge';
|
|
504
|
+
declare const x: string;
|
|
505
|
+
const y = isNull(x as string | null);
|
|
506
|
+
`,
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: 'isNullish on a value widened to include null is not narrowed to isUndefined',
|
|
510
|
+
code: dedent`
|
|
511
|
+
import { isNullish } from 'ts-data-forge';
|
|
512
|
+
declare const x: string | undefined;
|
|
513
|
+
const y = isNullish(x as string | null | undefined);
|
|
514
|
+
`,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
invalid: [
|
|
518
|
+
{
|
|
519
|
+
name: 'isNonNullish on a value whose `!` does not remove the null',
|
|
520
|
+
code: dedent`
|
|
521
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
522
|
+
declare const x: string | null;
|
|
523
|
+
const y = isNonNullish(x!);
|
|
524
|
+
`,
|
|
525
|
+
output: dedent`
|
|
526
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
527
|
+
declare const x: string | null;
|
|
528
|
+
const y = isNotNull(x!);
|
|
529
|
+
`,
|
|
530
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'isNull on a value widened to exclude null still, at every layer',
|
|
534
|
+
code: dedent`
|
|
535
|
+
import { isNull } from 'ts-data-forge';
|
|
536
|
+
declare const x: string;
|
|
537
|
+
const y = isNull(x as string | undefined);
|
|
538
|
+
`,
|
|
539
|
+
output: dedent`
|
|
540
|
+
import { isNull } from 'ts-data-forge';
|
|
541
|
+
declare const x: string;
|
|
542
|
+
const y = false;
|
|
543
|
+
`,
|
|
544
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: 'isNullish replaced on the value under `satisfies`, keeping the wrapper',
|
|
548
|
+
code: dedent`
|
|
549
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
550
|
+
declare const x: string | undefined;
|
|
551
|
+
const y = isNullish(x satisfies string | null | undefined);
|
|
552
|
+
`,
|
|
553
|
+
output: dedent`
|
|
554
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
555
|
+
declare const x: string | undefined;
|
|
556
|
+
const y = isUndefined(x satisfies string | null | undefined);
|
|
557
|
+
`,
|
|
558
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
});
|
|
562
|
+
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
7
|
import {
|
|
8
8
|
buildImportFixes,
|
|
9
9
|
getNamedImports,
|
|
@@ -65,15 +65,15 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
65
65
|
return [];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Only the callee is replaced: parentheses the source put around
|
|
69
|
+
// it are outside its range, and stay balanced.
|
|
70
70
|
// Add import only for the first node and only if not already imported
|
|
71
71
|
const importFixes =
|
|
72
72
|
index === 0 && !hasArrImport
|
|
73
73
|
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
74
74
|
: [];
|
|
75
75
|
|
|
76
|
-
return [...importFixes, fixer.replaceText(
|
|
76
|
+
return [...importFixes, fixer.replaceText(callee, 'Arr.isArray')];
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
79
|
}
|
|
@@ -84,13 +84,18 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
84
84
|
} as const;
|
|
85
85
|
|
|
86
86
|
const isArrayIsArrayCall = (
|
|
87
|
-
|
|
87
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
89
|
+
node: TSESTree.CallExpression,
|
|
88
90
|
): boolean => {
|
|
89
91
|
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
90
92
|
return false;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
const {
|
|
95
|
+
const { property } = node.callee;
|
|
96
|
+
|
|
97
|
+
// `(Array as ArrayConstructor).isArray` is still `Array.isArray`.
|
|
98
|
+
const object = skipTypeWrappers(node.callee.object);
|
|
94
99
|
|
|
95
100
|
if (object.type !== AST_NODE_TYPES.Identifier) {
|
|
96
101
|
return false;
|
|
@@ -60,3 +60,43 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
describe('prefer-arr-is-array through type wrappers', () => {
|
|
65
|
+
ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
66
|
+
valid: [],
|
|
67
|
+
invalid: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Array reached through `as`',
|
|
70
|
+
code: dedent`
|
|
71
|
+
import { Arr } from 'ts-data-forge';
|
|
72
|
+
const result = (Array as ArrayConstructor).isArray(value);
|
|
73
|
+
`,
|
|
74
|
+
output: dedent`
|
|
75
|
+
import { Arr } from 'ts-data-forge';
|
|
76
|
+
const result = Arr.isArray(value);
|
|
77
|
+
`,
|
|
78
|
+
errors: [{ messageId: 'useArrIsArray' }],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('prefer-arr-is-array with parenthesized operands', () => {
|
|
85
|
+
ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
86
|
+
valid: [],
|
|
87
|
+
invalid: [
|
|
88
|
+
{
|
|
89
|
+
name: 'a parenthesized callee',
|
|
90
|
+
code: dedent`
|
|
91
|
+
import { Arr } from 'ts-data-forge';
|
|
92
|
+
const result = (Array.isArray)(value);
|
|
93
|
+
`,
|
|
94
|
+
output: dedent`
|
|
95
|
+
import { Arr } from 'ts-data-forge';
|
|
96
|
+
const result = (Arr.isArray)(value);
|
|
97
|
+
`,
|
|
98
|
+
errors: [{ messageId: 'useArrIsArray' }],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import {
|
|
3
|
+
isArrayOrTupleExpression,
|
|
4
|
+
isSameArrayText,
|
|
5
|
+
parseLengthComparison,
|
|
6
|
+
toArgumentText,
|
|
7
|
+
} from './ast-utils.mjs';
|
|
3
8
|
import {
|
|
4
9
|
buildImportFixes,
|
|
5
10
|
getNamedImports,
|
|
@@ -64,9 +69,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
if (
|
|
68
|
-
sourceCode.getText(left.array) !== sourceCode.getText(right.array)
|
|
69
|
-
) {
|
|
72
|
+
if (!isSameArrayText(left.array, right.array, sourceCode)) {
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -77,25 +80,10 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
77
80
|
const arrayExpression = min.array;
|
|
78
81
|
|
|
79
82
|
// Check if arrayExpression is actually an array type
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (tsNode !== undefined) {
|
|
86
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
87
|
-
|
|
88
|
-
// Check if it's an array type or tuple type
|
|
89
|
-
const isArrayType =
|
|
90
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
91
|
-
|
|
92
|
-
if (!isArrayType) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
83
|
+
if (
|
|
84
|
+
!isArrayOrTupleExpression(services, min.array) ||
|
|
85
|
+
!isArrayOrTupleExpression(services, max.array)
|
|
86
|
+
) {
|
|
99
87
|
return;
|
|
100
88
|
}
|
|
101
89
|
|
|
@@ -115,7 +103,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
115
103
|
index,
|
|
116
104
|
{ node, arrayExpression, minExpression, maxExpression },
|
|
117
105
|
] of mut_nodesToFix.entries()) {
|
|
118
|
-
const arrayText =
|
|
106
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
119
107
|
|
|
120
108
|
const minText = sourceCode.getText(minExpression);
|
|
121
109
|
|
|
@@ -152,3 +152,71 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
152
152
|
},
|
|
153
153
|
);
|
|
154
154
|
}, 20000);
|
|
155
|
+
|
|
156
|
+
describe('prefer-arr-is-bounded-length-array through type wrappers', () => {
|
|
157
|
+
tester.run(
|
|
158
|
+
'prefer-arr-is-bounded-length-array',
|
|
159
|
+
preferArrIsBoundedLengthArray,
|
|
160
|
+
{
|
|
161
|
+
valid: [
|
|
162
|
+
{
|
|
163
|
+
name: 'a computed index named `length` is not the length',
|
|
164
|
+
code: dedent`
|
|
165
|
+
declare const xs: readonly number[];
|
|
166
|
+
declare const length: number;
|
|
167
|
+
const ok = xs[length] >= 1 && xs.length <= 3;
|
|
168
|
+
`,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
invalid: [
|
|
172
|
+
{
|
|
173
|
+
name: 'the array, the length and the bounds wrapped',
|
|
174
|
+
code: dedent`
|
|
175
|
+
import { Arr } from 'ts-data-forge';
|
|
176
|
+
declare const xs: readonly number[];
|
|
177
|
+
const a = xs.length >= 1 && (xs as readonly number[]).length <= 3;
|
|
178
|
+
const b = (xs.length satisfies number) >= (1 as number) && xs!.length <= 3;
|
|
179
|
+
`,
|
|
180
|
+
output: dedent`
|
|
181
|
+
import { Arr } from 'ts-data-forge';
|
|
182
|
+
declare const xs: readonly number[];
|
|
183
|
+
const a = Arr.isBoundedLengthArray(1, 3, xs);
|
|
184
|
+
const b = Arr.isBoundedLengthArray(1, 3, xs);
|
|
185
|
+
`,
|
|
186
|
+
errors: [
|
|
187
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
188
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
}, 20000);
|
|
195
|
+
|
|
196
|
+
describe('prefer-arr-is-bounded-length-array with parenthesized operands', () => {
|
|
197
|
+
tester.run(
|
|
198
|
+
'prefer-arr-is-bounded-length-array',
|
|
199
|
+
preferArrIsBoundedLengthArray,
|
|
200
|
+
{
|
|
201
|
+
valid: [],
|
|
202
|
+
invalid: [
|
|
203
|
+
{
|
|
204
|
+
name: 'a sequence array keeps its parentheses',
|
|
205
|
+
code: dedent`
|
|
206
|
+
import { Arr } from 'ts-data-forge';
|
|
207
|
+
declare const xs: readonly number[];
|
|
208
|
+
declare const log: () => void;
|
|
209
|
+
const ok = (log(), xs).length >= 1 && (log(), xs).length <= 3;
|
|
210
|
+
`,
|
|
211
|
+
output: dedent`
|
|
212
|
+
import { Arr } from 'ts-data-forge';
|
|
213
|
+
declare const xs: readonly number[];
|
|
214
|
+
declare const log: () => void;
|
|
215
|
+
const ok = Arr.isBoundedLengthArray(1, 3, (log(), xs));
|
|
216
|
+
`,
|
|
217
|
+
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
}, 20000);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
asLengthAccess,
|
|
4
|
+
isArrayOrTupleExpression,
|
|
5
|
+
isIntegerLiteralOrConstant,
|
|
6
|
+
skipTypeWrappers,
|
|
7
|
+
toArgumentText,
|
|
8
|
+
} from './ast-utils.mjs';
|
|
8
9
|
import {
|
|
9
10
|
buildImportFixes,
|
|
10
11
|
getNamedImports,
|
|
@@ -58,13 +59,16 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
58
59
|
|
|
59
60
|
const isNegated = node.operator === '!==';
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// Both sides are read through type wrappers.
|
|
63
|
+
const leftLength = asLengthAccess(node.left);
|
|
62
64
|
|
|
63
|
-
const
|
|
65
|
+
const isLengthOnLeft = leftLength !== undefined;
|
|
66
|
+
|
|
67
|
+
const lengthSide = leftLength ?? asLengthAccess(node.right);
|
|
64
68
|
|
|
65
69
|
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
66
70
|
|
|
67
|
-
if (
|
|
71
|
+
if (lengthSide === undefined) {
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -73,36 +77,19 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
73
77
|
return;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
// lengthSide is MemberExpression accessing .length
|
|
77
80
|
const arrayExpression = lengthSide.object;
|
|
78
81
|
|
|
79
82
|
// Check if arrayExpression is actually an array type
|
|
80
|
-
if (services
|
|
81
|
-
const checker = services.program.getTypeChecker();
|
|
82
|
-
|
|
83
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
84
|
-
|
|
85
|
-
if (tsNode !== undefined) {
|
|
86
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
87
|
-
|
|
88
|
-
// Check if it's an array type or tuple type
|
|
89
|
-
const isArrayType =
|
|
90
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
91
|
-
|
|
92
|
-
if (!isArrayType) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
83
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
99
84
|
return;
|
|
100
85
|
}
|
|
101
86
|
|
|
102
87
|
mut_nodesToFix.push({
|
|
103
88
|
node,
|
|
104
89
|
arrayExpression,
|
|
105
|
-
|
|
90
|
+
// The literal or constant itself: a wrapper such as `as number`
|
|
91
|
+
// would only widen the length the guard narrows to.
|
|
92
|
+
lengthExpression: skipTypeWrappers(valueSide),
|
|
106
93
|
isNegated,
|
|
107
94
|
});
|
|
108
95
|
},
|
|
@@ -115,7 +102,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
115
102
|
index,
|
|
116
103
|
{ node, arrayExpression, lengthExpression, isNegated },
|
|
117
104
|
] of mut_nodesToFix.entries()) {
|
|
118
|
-
const arrayText =
|
|
105
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
119
106
|
|
|
120
107
|
const lengthText = sourceCode.getText(lengthExpression);
|
|
121
108
|
|
|
@@ -148,10 +135,3 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
148
135
|
},
|
|
149
136
|
defaultOptions: [],
|
|
150
137
|
} as const;
|
|
151
|
-
|
|
152
|
-
const isLengthAccess = (
|
|
153
|
-
node: DeepReadonly<TSESTree.Expression>,
|
|
154
|
-
): node is TSESTree.MemberExpression =>
|
|
155
|
-
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
156
|
-
node.property.type === AST_NODE_TYPES.Identifier &&
|
|
157
|
-
node.property.name === 'length';
|
|
@@ -212,3 +212,66 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
212
212
|
],
|
|
213
213
|
});
|
|
214
214
|
}, 20000);
|
|
215
|
+
|
|
216
|
+
describe('prefer-arr-is-fixed-length-array through type wrappers', () => {
|
|
217
|
+
tester.run('prefer-arr-is-fixed-length-array', preferArrIsFixedLengthArray, {
|
|
218
|
+
valid: [
|
|
219
|
+
{
|
|
220
|
+
name: 'a computed index named `length` is not the length',
|
|
221
|
+
code: dedent`
|
|
222
|
+
declare const xs: readonly number[];
|
|
223
|
+
declare const length: number;
|
|
224
|
+
const ok = xs[length] === 3;
|
|
225
|
+
`,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
invalid: [
|
|
229
|
+
{
|
|
230
|
+
name: 'the length or the bound wrapped',
|
|
231
|
+
code: dedent`
|
|
232
|
+
import { Arr } from 'ts-data-forge';
|
|
233
|
+
declare const xs: readonly number[];
|
|
234
|
+
const a = xs.length === (3 satisfies number);
|
|
235
|
+
const b = (xs.length as number) !== 3;
|
|
236
|
+
const c = (2 as number) === xs.length;
|
|
237
|
+
`,
|
|
238
|
+
output: dedent`
|
|
239
|
+
import { Arr } from 'ts-data-forge';
|
|
240
|
+
declare const xs: readonly number[];
|
|
241
|
+
const a = Arr.isFixedLengthArray(3, xs);
|
|
242
|
+
const b = !Arr.isFixedLengthArray(3, xs);
|
|
243
|
+
const c = Arr.isFixedLengthArray(2, xs);
|
|
244
|
+
`,
|
|
245
|
+
errors: [
|
|
246
|
+
{ messageId: 'useIsFixedLengthArray' },
|
|
247
|
+
{ messageId: 'useIsFixedLengthArray' },
|
|
248
|
+
{ messageId: 'useIsFixedLengthArray' },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
}, 20000);
|
|
254
|
+
|
|
255
|
+
describe('prefer-arr-is-fixed-length-array with parenthesized operands', () => {
|
|
256
|
+
tester.run('prefer-arr-is-fixed-length-array', preferArrIsFixedLengthArray, {
|
|
257
|
+
valid: [],
|
|
258
|
+
invalid: [
|
|
259
|
+
{
|
|
260
|
+
name: 'a sequence array keeps its parentheses',
|
|
261
|
+
code: dedent`
|
|
262
|
+
import { Arr } from 'ts-data-forge';
|
|
263
|
+
declare const xs: readonly number[];
|
|
264
|
+
declare const log: () => void;
|
|
265
|
+
const ok = (log(), xs).length === 2;
|
|
266
|
+
`,
|
|
267
|
+
output: dedent`
|
|
268
|
+
import { Arr } from 'ts-data-forge';
|
|
269
|
+
declare const xs: readonly number[];
|
|
270
|
+
declare const log: () => void;
|
|
271
|
+
const ok = Arr.isFixedLengthArray(2, (log(), xs));
|
|
272
|
+
`,
|
|
273
|
+
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
}, 20000);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import {
|
|
3
|
+
asLengthAccess,
|
|
4
|
+
isArrayOrTupleExpression,
|
|
3
5
|
isIntegerLiteralOrConstant,
|
|
4
|
-
isLengthAccess,
|
|
5
6
|
isPartOfBoundedLengthCheck,
|
|
7
|
+
skipTypeWrappers,
|
|
8
|
+
toArgumentText,
|
|
6
9
|
} from './ast-utils.mjs';
|
|
7
10
|
import {
|
|
8
11
|
buildImportFixes,
|
|
@@ -63,12 +66,15 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
63
66
|
// xs.length <= n or n >= xs.length
|
|
64
67
|
const isLengthOnLeft = node.operator === '<=';
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
// Both sides are read through type wrappers.
|
|
70
|
+
const lengthSide = asLengthAccess(
|
|
71
|
+
node[isLengthOnLeft ? 'left' : 'right'],
|
|
72
|
+
);
|
|
67
73
|
|
|
68
74
|
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
69
75
|
|
|
70
76
|
// Check if lengthSide is accessing .length
|
|
71
|
-
if (
|
|
77
|
+
if (lengthSide === undefined) {
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -77,36 +83,19 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
77
83
|
return;
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
// lengthSide is MemberExpression accessing .length
|
|
81
86
|
const arrayExpression = lengthSide.object;
|
|
82
87
|
|
|
83
88
|
// Check if arrayExpression is actually an array type
|
|
84
|
-
if (services
|
|
85
|
-
const checker = services.program.getTypeChecker();
|
|
86
|
-
|
|
87
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
88
|
-
|
|
89
|
-
if (tsNode !== undefined) {
|
|
90
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
91
|
-
|
|
92
|
-
// Check if it's an array type or tuple type
|
|
93
|
-
const isArrayType =
|
|
94
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
95
|
-
|
|
96
|
-
if (!isArrayType) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
89
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
103
90
|
return;
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
mut_nodesToFix.push({
|
|
107
94
|
node,
|
|
108
95
|
arrayExpression,
|
|
109
|
-
|
|
96
|
+
// The literal or constant itself: a wrapper such as `as number`
|
|
97
|
+
// would only widen the length the guard narrows to.
|
|
98
|
+
lengthExpression: skipTypeWrappers(valueSide),
|
|
110
99
|
});
|
|
111
100
|
},
|
|
112
101
|
'Program:exit': () => {
|
|
@@ -118,7 +107,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
118
107
|
index,
|
|
119
108
|
{ node, arrayExpression, lengthExpression },
|
|
120
109
|
] of mut_nodesToFix.entries()) {
|
|
121
|
-
const arrayText =
|
|
110
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
122
111
|
|
|
123
112
|
const lengthText = sourceCode.getText(lengthExpression);
|
|
124
113
|
|
|
@@ -186,3 +186,70 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
186
186
|
],
|
|
187
187
|
});
|
|
188
188
|
}, 20000);
|
|
189
|
+
|
|
190
|
+
describe('prefer-arr-is-max-length-array through type wrappers', () => {
|
|
191
|
+
tester.run('prefer-arr-is-max-length-array', preferArrIsMaxLengthArray, {
|
|
192
|
+
valid: [
|
|
193
|
+
{
|
|
194
|
+
name: 'a computed index named `length` is not the length',
|
|
195
|
+
code: dedent`
|
|
196
|
+
declare const xs: readonly number[];
|
|
197
|
+
declare const length: number;
|
|
198
|
+
const ok = xs[length] <= 2;
|
|
199
|
+
`,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'the upper half of a bounded pair whose other half is wrapped',
|
|
203
|
+
code: dedent`
|
|
204
|
+
declare const xs: readonly number[];
|
|
205
|
+
const ok = xs!.length >= 1 && xs.length <= 3;
|
|
206
|
+
`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
invalid: [
|
|
210
|
+
{
|
|
211
|
+
name: 'the length or the bound wrapped',
|
|
212
|
+
code: dedent`
|
|
213
|
+
import { Arr } from 'ts-data-forge';
|
|
214
|
+
declare const xs: readonly number[];
|
|
215
|
+
const a = (xs.length satisfies number) <= 2;
|
|
216
|
+
const b = xs.length <= (2 as number);
|
|
217
|
+
`,
|
|
218
|
+
output: dedent`
|
|
219
|
+
import { Arr } from 'ts-data-forge';
|
|
220
|
+
declare const xs: readonly number[];
|
|
221
|
+
const a = Arr.isMaxLengthArray(2, xs);
|
|
222
|
+
const b = Arr.isMaxLengthArray(2, xs);
|
|
223
|
+
`,
|
|
224
|
+
errors: [
|
|
225
|
+
{ messageId: 'useIsMaxLengthArray' },
|
|
226
|
+
{ messageId: 'useIsMaxLengthArray' },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
}, 20000);
|
|
232
|
+
|
|
233
|
+
describe('prefer-arr-is-max-length-array with parenthesized operands', () => {
|
|
234
|
+
tester.run('prefer-arr-is-max-length-array', preferArrIsMaxLengthArray, {
|
|
235
|
+
valid: [],
|
|
236
|
+
invalid: [
|
|
237
|
+
{
|
|
238
|
+
name: 'a sequence array keeps its parentheses',
|
|
239
|
+
code: dedent`
|
|
240
|
+
import { Arr } from 'ts-data-forge';
|
|
241
|
+
declare const xs: readonly number[];
|
|
242
|
+
declare const log: () => void;
|
|
243
|
+
const ok = (log(), xs).length <= 2;
|
|
244
|
+
`,
|
|
245
|
+
output: dedent`
|
|
246
|
+
import { Arr } from 'ts-data-forge';
|
|
247
|
+
declare const xs: readonly number[];
|
|
248
|
+
declare const log: () => void;
|
|
249
|
+
const ok = Arr.isMaxLengthArray(2, (log(), xs));
|
|
250
|
+
`,
|
|
251
|
+
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
}, 20000);
|