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
|
@@ -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,
|
|
@@ -47,20 +52,24 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
47
52
|
return {
|
|
48
53
|
LogicalExpression: (node) => {
|
|
49
54
|
// Only `a && b` can encode a `min && max` length range.
|
|
50
|
-
if (node.operator !== '&&')
|
|
55
|
+
if (node.operator !== '&&') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
51
58
|
|
|
52
59
|
const left = parseLengthComparison(node.left, sourceCode);
|
|
53
60
|
|
|
54
61
|
const right = parseLengthComparison(node.right, sourceCode);
|
|
55
62
|
|
|
56
|
-
if (left === undefined || right === undefined)
|
|
63
|
+
if (left === undefined || right === undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
57
66
|
|
|
58
67
|
// Need one lower bound and one upper bound on the same array.
|
|
59
|
-
if (left.kind === right.kind)
|
|
68
|
+
if (left.kind === right.kind) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
60
71
|
|
|
61
|
-
if (
|
|
62
|
-
sourceCode.getText(left.array) !== sourceCode.getText(right.array)
|
|
63
|
-
) {
|
|
72
|
+
if (!isSameArrayText(left.array, right.array, sourceCode)) {
|
|
64
73
|
return;
|
|
65
74
|
}
|
|
66
75
|
|
|
@@ -71,23 +80,10 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
71
80
|
const arrayExpression = min.array;
|
|
72
81
|
|
|
73
82
|
// Check if arrayExpression is actually an array type
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (tsNode !== undefined) {
|
|
80
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
81
|
-
|
|
82
|
-
// Check if it's an array type or tuple type
|
|
83
|
-
const isArrayType =
|
|
84
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
85
|
-
|
|
86
|
-
if (!isArrayType) return;
|
|
87
|
-
} else {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
83
|
+
if (
|
|
84
|
+
!isArrayOrTupleExpression(services, min.array) ||
|
|
85
|
+
!isArrayOrTupleExpression(services, max.array)
|
|
86
|
+
) {
|
|
91
87
|
return;
|
|
92
88
|
}
|
|
93
89
|
|
|
@@ -107,7 +103,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
|
107
103
|
index,
|
|
108
104
|
{ node, arrayExpression, minExpression, maxExpression },
|
|
109
105
|
] of mut_nodesToFix.entries()) {
|
|
110
|
-
const arrayText =
|
|
106
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
111
107
|
|
|
112
108
|
const minText = sourceCode.getText(minExpression);
|
|
113
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,
|
|
@@ -52,49 +53,43 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
52
53
|
return {
|
|
53
54
|
BinaryExpression: (node) => {
|
|
54
55
|
// Check for `xs.length === n` or `n === xs.length` or `xs.length !== n`
|
|
55
|
-
if (node.operator !== '===' && node.operator !== '!==')
|
|
56
|
+
if (node.operator !== '===' && node.operator !== '!==') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
56
59
|
|
|
57
60
|
const isNegated = node.operator === '!==';
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Both sides are read through type wrappers.
|
|
63
|
+
const leftLength = asLengthAccess(node.left);
|
|
64
|
+
|
|
65
|
+
const isLengthOnLeft = leftLength !== undefined;
|
|
60
66
|
|
|
61
|
-
const lengthSide =
|
|
67
|
+
const lengthSide = leftLength ?? asLengthAccess(node.right);
|
|
62
68
|
|
|
63
69
|
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
64
70
|
|
|
65
|
-
if (
|
|
71
|
+
if (lengthSide === undefined) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
66
74
|
|
|
67
75
|
// Only match integer literals or const variables initialized with integer literals
|
|
68
|
-
if (!isIntegerLiteralOrConstant(valueSide, sourceCode))
|
|
76
|
+
if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
69
79
|
|
|
70
|
-
// lengthSide is MemberExpression accessing .length
|
|
71
80
|
const arrayExpression = lengthSide.object;
|
|
72
81
|
|
|
73
82
|
// Check if arrayExpression is actually an array type
|
|
74
|
-
if (services
|
|
75
|
-
const checker = services.program.getTypeChecker();
|
|
76
|
-
|
|
77
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
78
|
-
|
|
79
|
-
if (tsNode !== undefined) {
|
|
80
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
81
|
-
|
|
82
|
-
// Check if it's an array type or tuple type
|
|
83
|
-
const isArrayType =
|
|
84
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
85
|
-
|
|
86
|
-
if (!isArrayType) return;
|
|
87
|
-
} else {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
83
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
91
84
|
return;
|
|
92
85
|
}
|
|
93
86
|
|
|
94
87
|
mut_nodesToFix.push({
|
|
95
88
|
node,
|
|
96
89
|
arrayExpression,
|
|
97
|
-
|
|
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),
|
|
98
93
|
isNegated,
|
|
99
94
|
});
|
|
100
95
|
},
|
|
@@ -107,7 +102,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
107
102
|
index,
|
|
108
103
|
{ node, arrayExpression, lengthExpression, isNegated },
|
|
109
104
|
] of mut_nodesToFix.entries()) {
|
|
110
|
-
const arrayText =
|
|
105
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
111
106
|
|
|
112
107
|
const lengthText = sourceCode.getText(lengthExpression);
|
|
113
108
|
|
|
@@ -140,10 +135,3 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
140
135
|
},
|
|
141
136
|
defaultOptions: [],
|
|
142
137
|
} as const;
|
|
143
|
-
|
|
144
|
-
const isLengthAccess = (
|
|
145
|
-
node: DeepReadonly<TSESTree.Expression>,
|
|
146
|
-
): node is TSESTree.MemberExpression =>
|
|
147
|
-
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
148
|
-
node.property.type === AST_NODE_TYPES.Identifier &&
|
|
149
|
-
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,
|
|
@@ -50,53 +53,49 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
50
53
|
return {
|
|
51
54
|
BinaryExpression: (node) => {
|
|
52
55
|
// Check for `xs.length <= n` or `n >= xs.length`
|
|
53
|
-
if (node.operator !== '>=' && node.operator !== '<=')
|
|
56
|
+
if (node.operator !== '>=' && node.operator !== '<=') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
54
59
|
|
|
55
60
|
// Defer to `prefer-arr-is-bounded-length-array` when this comparison is
|
|
56
61
|
// the upper bound of a `min && max` pair on the same array.
|
|
57
|
-
if (isPartOfBoundedLengthCheck(node, sourceCode))
|
|
62
|
+
if (isPartOfBoundedLengthCheck(node, sourceCode)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
58
65
|
|
|
59
66
|
// xs.length <= n or n >= xs.length
|
|
60
67
|
const isLengthOnLeft = node.operator === '<=';
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
// Both sides are read through type wrappers.
|
|
70
|
+
const lengthSide = asLengthAccess(
|
|
71
|
+
node[isLengthOnLeft ? 'left' : 'right'],
|
|
72
|
+
);
|
|
63
73
|
|
|
64
74
|
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
65
75
|
|
|
66
76
|
// Check if lengthSide is accessing .length
|
|
67
|
-
if (
|
|
77
|
+
if (lengthSide === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
68
80
|
|
|
69
81
|
// Only match integer literals or const variables initialized with integer literals
|
|
70
|
-
if (!isIntegerLiteralOrConstant(valueSide, sourceCode))
|
|
82
|
+
if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
71
85
|
|
|
72
|
-
// lengthSide is MemberExpression accessing .length
|
|
73
86
|
const arrayExpression = lengthSide.object;
|
|
74
87
|
|
|
75
88
|
// Check if arrayExpression is actually an array type
|
|
76
|
-
if (services
|
|
77
|
-
const checker = services.program.getTypeChecker();
|
|
78
|
-
|
|
79
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
80
|
-
|
|
81
|
-
if (tsNode !== undefined) {
|
|
82
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
83
|
-
|
|
84
|
-
// Check if it's an array type or tuple type
|
|
85
|
-
const isArrayType =
|
|
86
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
87
|
-
|
|
88
|
-
if (!isArrayType) return;
|
|
89
|
-
} else {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
89
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
93
90
|
return;
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
mut_nodesToFix.push({
|
|
97
94
|
node,
|
|
98
95
|
arrayExpression,
|
|
99
|
-
|
|
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),
|
|
100
99
|
});
|
|
101
100
|
},
|
|
102
101
|
'Program:exit': () => {
|
|
@@ -108,7 +107,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
108
107
|
index,
|
|
109
108
|
{ node, arrayExpression, lengthExpression },
|
|
110
109
|
] of mut_nodesToFix.entries()) {
|
|
111
|
-
const arrayText =
|
|
110
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
112
111
|
|
|
113
112
|
const lengthText = sourceCode.getText(lengthExpression);
|
|
114
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);
|
|
@@ -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,
|
|
@@ -50,53 +53,49 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
|
|
|
50
53
|
return {
|
|
51
54
|
BinaryExpression: (node) => {
|
|
52
55
|
// Check for `xs.length >= n` or `n <= xs.length`
|
|
53
|
-
if (node.operator !== '>=' && node.operator !== '<=')
|
|
56
|
+
if (node.operator !== '>=' && node.operator !== '<=') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
54
59
|
|
|
55
60
|
// Defer to `prefer-arr-is-bounded-length-array` when this comparison is
|
|
56
61
|
// the lower bound of a `min && max` pair on the same array.
|
|
57
|
-
if (isPartOfBoundedLengthCheck(node, sourceCode))
|
|
62
|
+
if (isPartOfBoundedLengthCheck(node, sourceCode)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
58
65
|
|
|
59
66
|
// xs.length >= n or n <= xs.length
|
|
60
67
|
const isLengthOnLeft = node.operator === '>=';
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
// Both sides are read through type wrappers.
|
|
70
|
+
const lengthSide = asLengthAccess(
|
|
71
|
+
node[isLengthOnLeft ? 'left' : 'right'],
|
|
72
|
+
);
|
|
63
73
|
|
|
64
74
|
const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
|
|
65
75
|
|
|
66
76
|
// Check if lengthSide is accessing .length
|
|
67
|
-
if (
|
|
77
|
+
if (lengthSide === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
68
80
|
|
|
69
81
|
// Only match integer literals or const variables initialized with integer literals
|
|
70
|
-
if (!isIntegerLiteralOrConstant(valueSide, sourceCode))
|
|
82
|
+
if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
71
85
|
|
|
72
|
-
// lengthSide is MemberExpression accessing .length
|
|
73
86
|
const arrayExpression = lengthSide.object;
|
|
74
87
|
|
|
75
88
|
// Check if arrayExpression is actually an array type
|
|
76
|
-
if (services
|
|
77
|
-
const checker = services.program.getTypeChecker();
|
|
78
|
-
|
|
79
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
80
|
-
|
|
81
|
-
if (tsNode !== undefined) {
|
|
82
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
83
|
-
|
|
84
|
-
// Check if it's an array type or tuple type
|
|
85
|
-
const isArrayType =
|
|
86
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
87
|
-
|
|
88
|
-
if (!isArrayType) return;
|
|
89
|
-
} else {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
89
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
93
90
|
return;
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
mut_nodesToFix.push({
|
|
97
94
|
node,
|
|
98
95
|
arrayExpression,
|
|
99
|
-
|
|
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),
|
|
100
99
|
});
|
|
101
100
|
},
|
|
102
101
|
'Program:exit': () => {
|
|
@@ -108,7 +107,7 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
|
|
|
108
107
|
index,
|
|
109
108
|
{ node, arrayExpression, lengthExpression },
|
|
110
109
|
] of mut_nodesToFix.entries()) {
|
|
111
|
-
const arrayText =
|
|
110
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
112
111
|
|
|
113
112
|
const lengthText = sourceCode.getText(lengthExpression);
|
|
114
113
|
|