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
|
@@ -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 preferArrIsMinLengthArray: 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 preferArrIsMinLengthArray: 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 preferArrIsMinLengthArray: 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
|
|
|
@@ -200,3 +200,73 @@ describe('prefer-arr-is-min-length-array', () => {
|
|
|
200
200
|
],
|
|
201
201
|
});
|
|
202
202
|
}, 20000);
|
|
203
|
+
|
|
204
|
+
describe('prefer-arr-is-min-length-array through type wrappers', () => {
|
|
205
|
+
tester.run('prefer-arr-is-min-length-array', preferArrIsMinLengthArray, {
|
|
206
|
+
valid: [
|
|
207
|
+
{
|
|
208
|
+
name: 'a computed index named `length` is not the length',
|
|
209
|
+
code: dedent`
|
|
210
|
+
declare const xs: readonly number[];
|
|
211
|
+
declare const length: number;
|
|
212
|
+
const ok = xs[length] >= 2;
|
|
213
|
+
`,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'the lower half of a bounded pair whose other half is wrapped',
|
|
217
|
+
code: dedent`
|
|
218
|
+
declare const xs: readonly number[];
|
|
219
|
+
const ok = xs.length >= 1 && (xs as readonly number[]).length <= 3;
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
invalid: [
|
|
224
|
+
{
|
|
225
|
+
name: 'the length or the bound wrapped',
|
|
226
|
+
code: dedent`
|
|
227
|
+
import { Arr } from 'ts-data-forge';
|
|
228
|
+
declare const xs: readonly number[];
|
|
229
|
+
const a = (xs.length satisfies number) >= 2;
|
|
230
|
+
const b = xs.length >= (2 as number);
|
|
231
|
+
const c = (3 satisfies number) <= xs.length!;
|
|
232
|
+
`,
|
|
233
|
+
output: dedent`
|
|
234
|
+
import { Arr } from 'ts-data-forge';
|
|
235
|
+
declare const xs: readonly number[];
|
|
236
|
+
const a = Arr.isMinLengthArray(2, xs);
|
|
237
|
+
const b = Arr.isMinLengthArray(2, xs);
|
|
238
|
+
const c = Arr.isMinLengthArray(3, xs);
|
|
239
|
+
`,
|
|
240
|
+
errors: [
|
|
241
|
+
{ messageId: 'useIsMinLengthArray' },
|
|
242
|
+
{ messageId: 'useIsMinLengthArray' },
|
|
243
|
+
{ messageId: 'useIsMinLengthArray' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
}, 20000);
|
|
249
|
+
|
|
250
|
+
describe('prefer-arr-is-min-length-array with parenthesized operands', () => {
|
|
251
|
+
tester.run('prefer-arr-is-min-length-array', preferArrIsMinLengthArray, {
|
|
252
|
+
valid: [],
|
|
253
|
+
invalid: [
|
|
254
|
+
{
|
|
255
|
+
name: 'a sequence array keeps its parentheses',
|
|
256
|
+
code: dedent`
|
|
257
|
+
import { Arr } from 'ts-data-forge';
|
|
258
|
+
declare const xs: readonly number[];
|
|
259
|
+
declare const log: () => void;
|
|
260
|
+
const ok = (log(), xs).length >= 2;
|
|
261
|
+
`,
|
|
262
|
+
output: dedent`
|
|
263
|
+
import { Arr } from 'ts-data-forge';
|
|
264
|
+
declare const xs: readonly number[];
|
|
265
|
+
declare const log: () => void;
|
|
266
|
+
const ok = Arr.isMinLengthArray(2, (log(), xs));
|
|
267
|
+
`,
|
|
268
|
+
errors: [{ messageId: 'useIsMinLengthArray' }],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
}, 20000);
|
|
@@ -3,6 +3,12 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import {
|
|
7
|
+
asLengthAccess,
|
|
8
|
+
isArrayOrTupleExpression,
|
|
9
|
+
skipTypeWrappers,
|
|
10
|
+
toArgumentText,
|
|
11
|
+
} from './ast-utils.mjs';
|
|
6
12
|
import {
|
|
7
13
|
buildImportFixes,
|
|
8
14
|
getNamedImports,
|
|
@@ -51,15 +57,18 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
51
57
|
|
|
52
58
|
const isLengthOnLeft = node.operator === '>';
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
// Both sides are read through type wrappers.
|
|
61
|
+
const lengthSide = asLengthAccess(
|
|
62
|
+
node[isLengthOnLeft ? 'left' : 'right'],
|
|
63
|
+
);
|
|
55
64
|
|
|
56
|
-
const numberSide =
|
|
65
|
+
const numberSide = skipTypeWrappers(
|
|
66
|
+
node[isLengthOnLeft ? 'right' : 'left'],
|
|
67
|
+
);
|
|
57
68
|
|
|
58
69
|
// Check if one side is `.length` and the other is 0
|
|
59
70
|
if (
|
|
60
|
-
lengthSide
|
|
61
|
-
lengthSide.property.type !== AST_NODE_TYPES.Identifier ||
|
|
62
|
-
lengthSide.property.name !== 'length' ||
|
|
71
|
+
lengthSide === undefined ||
|
|
63
72
|
numberSide.type !== AST_NODE_TYPES.Literal ||
|
|
64
73
|
numberSide.value !== 0
|
|
65
74
|
) {
|
|
@@ -69,25 +78,7 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
69
78
|
const arrayExpression = lengthSide.object;
|
|
70
79
|
|
|
71
80
|
// Check if arrayExpression is actually an array type
|
|
72
|
-
if (services
|
|
73
|
-
const checker = services.program.getTypeChecker();
|
|
74
|
-
|
|
75
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
76
|
-
|
|
77
|
-
if (tsNode !== undefined) {
|
|
78
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
79
|
-
|
|
80
|
-
// Check if it's an array type or tuple type
|
|
81
|
-
const isArrayType =
|
|
82
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
83
|
-
|
|
84
|
-
if (!isArrayType) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
81
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
91
82
|
return;
|
|
92
83
|
}
|
|
93
84
|
|
|
@@ -105,7 +96,7 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
105
96
|
index,
|
|
106
97
|
{ node, arrayExpression },
|
|
107
98
|
] of mut_nodesToFix.entries()) {
|
|
108
|
-
const arrayText =
|
|
99
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
109
100
|
|
|
110
101
|
const originalText = sourceCode.getText(node);
|
|
111
102
|
|
|
@@ -88,3 +88,82 @@ describe('prefer-arr-is-non-empty', () => {
|
|
|
88
88
|
],
|
|
89
89
|
});
|
|
90
90
|
}, 20000);
|
|
91
|
+
|
|
92
|
+
describe('prefer-arr-is-non-empty through type wrappers', () => {
|
|
93
|
+
tester.run('prefer-arr-is-non-empty', preferArrIsNonEmpty, {
|
|
94
|
+
valid: [
|
|
95
|
+
{
|
|
96
|
+
name: 'a type that says `0` is not the value `0`: only a literal is read through its wrapper',
|
|
97
|
+
code: dedent`
|
|
98
|
+
declare const xs: readonly number[];
|
|
99
|
+
declare const n: number;
|
|
100
|
+
const a = xs.length > (n as 0);
|
|
101
|
+
`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'a computed index named `length` is not the length',
|
|
105
|
+
code: dedent`
|
|
106
|
+
declare const xs: readonly number[];
|
|
107
|
+
declare const length: number;
|
|
108
|
+
const a = xs[length] > 0;
|
|
109
|
+
const b = xs[length]! > 0;
|
|
110
|
+
`,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'a string cast to an array',
|
|
114
|
+
code: dedent`
|
|
115
|
+
declare const s: string;
|
|
116
|
+
const ok = (s as unknown as readonly string[]).length > 0;
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
invalid: [
|
|
121
|
+
{
|
|
122
|
+
name: 'the length or the zero wrapped',
|
|
123
|
+
code: dedent`
|
|
124
|
+
import { Arr } from 'ts-data-forge';
|
|
125
|
+
declare const xs: readonly number[];
|
|
126
|
+
const a = (xs.length satisfies number) > 0;
|
|
127
|
+
const b = xs.length > (0 as number);
|
|
128
|
+
const c = 0 < xs.length!;
|
|
129
|
+
`,
|
|
130
|
+
output: dedent`
|
|
131
|
+
import { Arr } from 'ts-data-forge';
|
|
132
|
+
declare const xs: readonly number[];
|
|
133
|
+
const a = Arr.isNonEmpty(xs);
|
|
134
|
+
const b = Arr.isNonEmpty(xs);
|
|
135
|
+
const c = Arr.isNonEmpty(xs);
|
|
136
|
+
`,
|
|
137
|
+
errors: [
|
|
138
|
+
{ messageId: 'useIsNonEmpty' },
|
|
139
|
+
{ messageId: 'useIsNonEmpty' },
|
|
140
|
+
{ messageId: 'useIsNonEmpty' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
}, 20000);
|
|
146
|
+
|
|
147
|
+
describe('prefer-arr-is-non-empty with parenthesized operands', () => {
|
|
148
|
+
tester.run('prefer-arr-is-non-empty', preferArrIsNonEmpty, {
|
|
149
|
+
valid: [],
|
|
150
|
+
invalid: [
|
|
151
|
+
{
|
|
152
|
+
name: 'a sequence array keeps its parentheses',
|
|
153
|
+
code: dedent`
|
|
154
|
+
import { Arr } from 'ts-data-forge';
|
|
155
|
+
declare const xs: readonly number[];
|
|
156
|
+
declare const log: () => void;
|
|
157
|
+
const ok = (log(), xs).length > 0;
|
|
158
|
+
`,
|
|
159
|
+
output: dedent`
|
|
160
|
+
import { Arr } from 'ts-data-forge';
|
|
161
|
+
declare const xs: readonly number[];
|
|
162
|
+
declare const log: () => void;
|
|
163
|
+
const ok = Arr.isNonEmpty((log(), xs));
|
|
164
|
+
`,
|
|
165
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
}, 20000);
|
|
@@ -4,8 +4,13 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { Arr } from 'ts-data-forge';
|
|
7
|
-
import {
|
|
7
|
+
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
8
8
|
import * as ts from 'typescript';
|
|
9
|
+
import {
|
|
10
|
+
skipTypeWrappers,
|
|
11
|
+
toArgumentText,
|
|
12
|
+
typeWrapperLayers,
|
|
13
|
+
} from './ast-utils.mjs';
|
|
9
14
|
import {
|
|
10
15
|
buildImportFixes,
|
|
11
16
|
getNamedImports,
|
|
@@ -135,10 +140,10 @@ type Splice = Readonly<{ range: FixedLengthTuple<2, number>; text: string }>;
|
|
|
135
140
|
* fourth parameter (the whole array) having no counterpart in `scan`.
|
|
136
141
|
*
|
|
137
142
|
* `acc.at(-1)` is typed `T | undefined` while `scan`'s accumulator is `S`, so a
|
|
138
|
-
* `?? fallback` guarding it becomes redundant after the fix. It is
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
143
|
+
* `?? fallback` guarding it becomes redundant after the fix. It is removed only
|
|
144
|
+
* when type information shows that neither the seed nor the appended value can
|
|
145
|
+
* be nullish — asked of each both as written and under any `as` / `!`, since
|
|
146
|
+
* `[init!]` may still hold `undefined` — and kept otherwise.
|
|
142
147
|
*
|
|
143
148
|
* ## What is not reported
|
|
144
149
|
*
|
|
@@ -147,7 +152,8 @@ type Splice = Readonly<{ range: FixedLengthTuple<2, number>; text: string }>;
|
|
|
147
152
|
* `scan` hands its callback the accumulated value and the element, never the
|
|
148
153
|
* prefix, so such a callback is not one it can express.
|
|
149
154
|
*
|
|
150
|
-
* The array has to be spelled the same way in the `map` and in the `slice
|
|
155
|
+
* The array has to be spelled the same way in the `map` and in the `slice`,
|
|
156
|
+
* type wrappers aside (`(xs as readonly T[]).slice(…)` slices `xs`).
|
|
151
157
|
* Deciding that two differently written expressions denote one array needs type
|
|
152
158
|
* information and would still be a guess about aliasing.
|
|
153
159
|
*/
|
|
@@ -278,8 +284,14 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
278
284
|
// which a seeded accumulator never does. So once the value `scan` carries
|
|
279
285
|
// is known not to be nullish, a `?? fallback` guarding the read is dead
|
|
280
286
|
// and the fix takes it with the read — leaving it would hand the author an
|
|
281
|
-
// unnecessary condition that no rule can fix for them.
|
|
282
|
-
|
|
287
|
+
// unnecessary condition that no rule can fix for them. Every layer of
|
|
288
|
+
// type wrappers is asked (see typeWrapperLayers): the value under an
|
|
289
|
+
// `as` / `!` is what `scan` carries at run time (`[init!]` may still
|
|
290
|
+
// hold `undefined`), and a wider type a wrapper states is the author
|
|
291
|
+
// saying it may be nullish.
|
|
292
|
+
const carriesNullish = [seed, element].some((node) =>
|
|
293
|
+
typeWrapperLayers(node).some(isNullish),
|
|
294
|
+
);
|
|
283
295
|
|
|
284
296
|
const mut_lastReads: Splice[] = [];
|
|
285
297
|
|
|
@@ -331,12 +343,22 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
331
343
|
mut_lastReads,
|
|
332
344
|
);
|
|
333
345
|
|
|
334
|
-
const arrayText =
|
|
346
|
+
const arrayText = toArgumentText(
|
|
335
347
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
336
348
|
(call.callee as TSESTree.MemberExpression).object,
|
|
349
|
+
sourceCode,
|
|
337
350
|
);
|
|
338
351
|
|
|
339
|
-
|
|
352
|
+
// A node's text never includes the parentheses the source put around it:
|
|
353
|
+
// an object literal would open a block body, and a sequence would run on
|
|
354
|
+
// into the next argument.
|
|
355
|
+
const bodyText =
|
|
356
|
+
elementText.trimStart().startsWith('{') ||
|
|
357
|
+
element.type === AST_NODE_TYPES.SequenceExpression
|
|
358
|
+
? `(${elementText})`
|
|
359
|
+
: elementText;
|
|
360
|
+
|
|
361
|
+
return `Arr.scan(${arrayText}, (${parameterText}) => ${bodyText}, ${toArgumentText(seed, sourceCode)})`;
|
|
340
362
|
};
|
|
341
363
|
|
|
342
364
|
/**
|
|
@@ -397,7 +419,11 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
397
419
|
|
|
398
420
|
const methodName = node.callee.property.name;
|
|
399
421
|
|
|
400
|
-
|
|
422
|
+
// Through type wrappers, so that `(xs as readonly T[]).slice(…)` is
|
|
423
|
+
// seen slicing the `xs` being mapped.
|
|
424
|
+
const arrayText = sourceCode.getText(
|
|
425
|
+
skipTypeWrappers(node.callee.object),
|
|
426
|
+
);
|
|
401
427
|
|
|
402
428
|
if (SLICING_METHODS.has(methodName)) {
|
|
403
429
|
mut_slices.push({ call: node, arrayText });
|
|
@@ -651,9 +677,13 @@ const appendsToItsAccumulator = (
|
|
|
651
677
|
: undefined;
|
|
652
678
|
|
|
653
679
|
const target =
|
|
654
|
-
spelling
|
|
655
|
-
?
|
|
656
|
-
:
|
|
680
|
+
spelling === undefined || spelling === null
|
|
681
|
+
? undefined
|
|
682
|
+
: skipTypeWrappers(
|
|
683
|
+
spelling.type === AST_NODE_TYPES.SpreadElement
|
|
684
|
+
? spelling.argument
|
|
685
|
+
: spelling,
|
|
686
|
+
);
|
|
657
687
|
|
|
658
688
|
return (
|
|
659
689
|
target?.type === AST_NODE_TYPES.Identifier &&
|
|
@@ -690,13 +720,17 @@ const accumulationOf = (
|
|
|
690
720
|
|
|
691
721
|
const [spread, element] = body.elements;
|
|
692
722
|
|
|
723
|
+
const spreadTarget =
|
|
724
|
+
spread?.type === AST_NODE_TYPES.SpreadElement
|
|
725
|
+
? skipTypeWrappers(spread.argument)
|
|
726
|
+
: undefined;
|
|
727
|
+
|
|
693
728
|
return element === null ||
|
|
694
729
|
element.type === AST_NODE_TYPES.SpreadElement ||
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
spread.argument.name !== accumulatorParam.name
|
|
730
|
+
spreadTarget?.type !== AST_NODE_TYPES.Identifier ||
|
|
731
|
+
spreadTarget.name !== accumulatorParam.name
|
|
698
732
|
? undefined
|
|
699
|
-
: { accumulatorParam, accumulatorRef:
|
|
733
|
+
: { accumulatorParam, accumulatorRef: spreadTarget, element };
|
|
700
734
|
}
|
|
701
735
|
|
|
702
736
|
// `Arr.toPushed(acc, ELEMENT)`, matched on the method name as the rest of
|
|
@@ -714,7 +748,9 @@ const accumulationOf = (
|
|
|
714
748
|
return undefined;
|
|
715
749
|
}
|
|
716
750
|
|
|
717
|
-
const [
|
|
751
|
+
const [wrappedTarget, element] = body.arguments;
|
|
752
|
+
|
|
753
|
+
const target = skipTypeWrappers(wrappedTarget);
|
|
718
754
|
|
|
719
755
|
return element.type === AST_NODE_TYPES.SpreadElement ||
|
|
720
756
|
target.type !== AST_NODE_TYPES.Identifier ||
|
|
@@ -293,3 +293,152 @@ describe('prefer-arr-scan', () => {
|
|
|
293
293
|
],
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
|
+
|
|
297
|
+
describe('prefer-arr-scan through type wrappers', () => {
|
|
298
|
+
tester.run('prefer-arr-scan', preferArrScan, {
|
|
299
|
+
valid: [],
|
|
300
|
+
invalid: [
|
|
301
|
+
{
|
|
302
|
+
name: 'a seed whose `!` hides that it may be undefined keeps the `??`',
|
|
303
|
+
code: dedent`
|
|
304
|
+
import { Arr } from 'ts-data-forge';
|
|
305
|
+
declare const xs: readonly number[];
|
|
306
|
+
declare const init: number | undefined;
|
|
307
|
+
const totals = xs.reduce(
|
|
308
|
+
(acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
|
|
309
|
+
[init!],
|
|
310
|
+
);
|
|
311
|
+
`,
|
|
312
|
+
output: dedent`
|
|
313
|
+
import { Arr } from 'ts-data-forge';
|
|
314
|
+
declare const xs: readonly number[];
|
|
315
|
+
declare const init: number | undefined;
|
|
316
|
+
const totals = Arr.scan(xs, (acc, x) => (acc ?? 0) + x, init!);
|
|
317
|
+
`,
|
|
318
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'an element cast away from undefined keeps the `??`',
|
|
322
|
+
code: dedent`
|
|
323
|
+
import { Arr } from 'ts-data-forge';
|
|
324
|
+
declare const xs: readonly (number | undefined)[];
|
|
325
|
+
const totals = xs.reduce<readonly number[]>(
|
|
326
|
+
(acc, x) => [...acc, ((acc.at(-1) ?? 0) && x) as number],
|
|
327
|
+
[0],
|
|
328
|
+
);
|
|
329
|
+
`,
|
|
330
|
+
output: dedent`
|
|
331
|
+
import { Arr } from 'ts-data-forge';
|
|
332
|
+
declare const xs: readonly (number | undefined)[];
|
|
333
|
+
const totals = Arr.scan(xs, (acc, x) => ((acc ?? 0) && x) as number, 0);
|
|
334
|
+
`,
|
|
335
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'a spread accumulator wrapped in `as`',
|
|
339
|
+
code: dedent`
|
|
340
|
+
import { Arr } from 'ts-data-forge';
|
|
341
|
+
declare const xs: readonly number[];
|
|
342
|
+
const totals = xs.reduce<readonly number[]>(
|
|
343
|
+
(acc, x) => [...(acc as readonly number[]), (acc.at(-1) ?? 0) + x],
|
|
344
|
+
[0],
|
|
345
|
+
);
|
|
346
|
+
`,
|
|
347
|
+
output: dedent`
|
|
348
|
+
import { Arr } from 'ts-data-forge';
|
|
349
|
+
declare const xs: readonly number[];
|
|
350
|
+
const totals = Arr.scan(xs, (acc, x) => (acc) + x, 0);
|
|
351
|
+
`,
|
|
352
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: 'an Arr.toPushed target wrapped in `!`',
|
|
356
|
+
code: dedent`
|
|
357
|
+
import { Arr } from 'ts-data-forge';
|
|
358
|
+
declare const xs: readonly number[];
|
|
359
|
+
const totals = xs.reduce<readonly number[]>(
|
|
360
|
+
(acc, x) => Arr.toPushed(acc!, (acc.at(-1) ?? 0) + x),
|
|
361
|
+
[0],
|
|
362
|
+
);
|
|
363
|
+
`,
|
|
364
|
+
output: dedent`
|
|
365
|
+
import { Arr } from 'ts-data-forge';
|
|
366
|
+
declare const xs: readonly number[];
|
|
367
|
+
const totals = Arr.scan(xs, (acc, x) => (acc) + x, 0);
|
|
368
|
+
`,
|
|
369
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'the sliced array wrapped in `as`',
|
|
373
|
+
code: dedent`
|
|
374
|
+
declare const xs: readonly string[];
|
|
375
|
+
const ys = xs.map((_, i) =>
|
|
376
|
+
(xs as readonly string[]).slice(0, i + 1).join(''),
|
|
377
|
+
);
|
|
378
|
+
`,
|
|
379
|
+
errors: [{ messageId: 'preferArrScanPrefix' }],
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('prefer-arr-scan with parenthesized operands', () => {
|
|
386
|
+
tester.run('prefer-arr-scan', preferArrScan, {
|
|
387
|
+
valid: [],
|
|
388
|
+
invalid: [
|
|
389
|
+
{
|
|
390
|
+
name: 'an object literal element stays an expression body',
|
|
391
|
+
code: dedent`
|
|
392
|
+
import { Arr } from 'ts-data-forge';
|
|
393
|
+
declare const xs: readonly number[];
|
|
394
|
+
const totals = xs.reduce(
|
|
395
|
+
(acc, x) => [...acc, { sum: acc.at(-1)!.sum + x }],
|
|
396
|
+
[{ sum: 0 }],
|
|
397
|
+
);
|
|
398
|
+
`,
|
|
399
|
+
output: dedent`
|
|
400
|
+
import { Arr } from 'ts-data-forge';
|
|
401
|
+
declare const xs: readonly number[];
|
|
402
|
+
const totals = Arr.scan(xs, (acc, x) => ({ sum: acc!.sum + x }), { sum: 0 });
|
|
403
|
+
`,
|
|
404
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'an object literal element under a wrapper stays an expression body',
|
|
408
|
+
code: dedent`
|
|
409
|
+
import { Arr } from 'ts-data-forge';
|
|
410
|
+
declare const xs: readonly number[];
|
|
411
|
+
const totals = xs.reduce(
|
|
412
|
+
(acc, x) => [...acc, { sum: acc.at(-1)!.sum + x } as const],
|
|
413
|
+
[{ sum: 0 }],
|
|
414
|
+
);
|
|
415
|
+
`,
|
|
416
|
+
output: dedent`
|
|
417
|
+
import { Arr } from 'ts-data-forge';
|
|
418
|
+
declare const xs: readonly number[];
|
|
419
|
+
const totals = Arr.scan(xs, (acc, x) => ({ sum: acc!.sum + x } as const), { sum: 0 });
|
|
420
|
+
`,
|
|
421
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: 'a sequence element and seed keep their parentheses',
|
|
425
|
+
code: dedent`
|
|
426
|
+
import { Arr } from 'ts-data-forge';
|
|
427
|
+
declare const xs: readonly number[];
|
|
428
|
+
declare const log: (n: number) => void;
|
|
429
|
+
const totals = xs.reduce<readonly number[]>(
|
|
430
|
+
(acc, x) => [...acc, (log(x), (acc.at(-1) ?? 0) + x)],
|
|
431
|
+
[(log(0), 0)],
|
|
432
|
+
);
|
|
433
|
+
`,
|
|
434
|
+
output: dedent`
|
|
435
|
+
import { Arr } from 'ts-data-forge';
|
|
436
|
+
declare const xs: readonly number[];
|
|
437
|
+
declare const log: (n: number) => void;
|
|
438
|
+
const totals = Arr.scan(xs, (acc, x) => (log(x), (acc) + x), (log(0), 0));
|
|
439
|
+
`,
|
|
440
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
});
|