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
|
@@ -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,
|
|
@@ -45,19 +51,24 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
45
51
|
return {
|
|
46
52
|
BinaryExpression: (node) => {
|
|
47
53
|
// Check for `xs.length > 0` or `0 < xs.length`
|
|
48
|
-
if (node.operator !== '>' && node.operator !== '<')
|
|
54
|
+
if (node.operator !== '>' && node.operator !== '<') {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
49
57
|
|
|
50
58
|
const isLengthOnLeft = node.operator === '>';
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
// Both sides are read through type wrappers.
|
|
61
|
+
const lengthSide = asLengthAccess(
|
|
62
|
+
node[isLengthOnLeft ? 'left' : 'right'],
|
|
63
|
+
);
|
|
53
64
|
|
|
54
|
-
const numberSide =
|
|
65
|
+
const numberSide = skipTypeWrappers(
|
|
66
|
+
node[isLengthOnLeft ? 'right' : 'left'],
|
|
67
|
+
);
|
|
55
68
|
|
|
56
69
|
// Check if one side is `.length` and the other is 0
|
|
57
70
|
if (
|
|
58
|
-
lengthSide
|
|
59
|
-
lengthSide.property.type !== AST_NODE_TYPES.Identifier ||
|
|
60
|
-
lengthSide.property.name !== 'length' ||
|
|
71
|
+
lengthSide === undefined ||
|
|
61
72
|
numberSide.type !== AST_NODE_TYPES.Literal ||
|
|
62
73
|
numberSide.value !== 0
|
|
63
74
|
) {
|
|
@@ -67,23 +78,7 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
67
78
|
const arrayExpression = lengthSide.object;
|
|
68
79
|
|
|
69
80
|
// Check if arrayExpression is actually an array type
|
|
70
|
-
if (services
|
|
71
|
-
const checker = services.program.getTypeChecker();
|
|
72
|
-
|
|
73
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
74
|
-
|
|
75
|
-
if (tsNode !== undefined) {
|
|
76
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
77
|
-
|
|
78
|
-
// Check if it's an array type or tuple type
|
|
79
|
-
const isArrayType =
|
|
80
|
-
checker.isArrayType(type) || checker.isTupleType(type);
|
|
81
|
-
|
|
82
|
-
if (!isArrayType) return;
|
|
83
|
-
} else {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
81
|
+
if (!isArrayOrTupleExpression(services, arrayExpression)) {
|
|
87
82
|
return;
|
|
88
83
|
}
|
|
89
84
|
|
|
@@ -101,7 +96,7 @@ export const preferArrIsNonEmpty: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
101
96
|
index,
|
|
102
97
|
{ node, arrayExpression },
|
|
103
98
|
] of mut_nodesToFix.entries()) {
|
|
104
|
-
const arrayText =
|
|
99
|
+
const arrayText = toArgumentText(arrayExpression, sourceCode);
|
|
105
100
|
|
|
106
101
|
const originalText = sourceCode.getText(node);
|
|
107
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
|
*/
|
|
@@ -190,7 +196,9 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
190
196
|
): boolean => {
|
|
191
197
|
const tsNode = services?.esTreeNodeToTSNodeMap?.get(node);
|
|
192
198
|
|
|
193
|
-
if (checker === undefined || tsNode === undefined)
|
|
199
|
+
if (checker === undefined || tsNode === undefined) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
194
202
|
|
|
195
203
|
const type = checker.getTypeAtLocation(tsNode);
|
|
196
204
|
|
|
@@ -227,14 +235,16 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
227
235
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
228
236
|
accumulation: Accumulation,
|
|
229
237
|
): string | undefined => {
|
|
230
|
-
if (callback.async || callback.generator)
|
|
238
|
+
if (callback.async || callback.generator) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
231
241
|
|
|
232
242
|
// `reduce`'s fourth parameter is the whole array, which `scan` never
|
|
233
243
|
// hands its callback; an annotation on the accumulator describes that
|
|
234
244
|
// array, and would be wrong on `scan`'s value parameter.
|
|
235
245
|
if (
|
|
236
246
|
callback.params.length < 2 ||
|
|
237
|
-
callback.params.length
|
|
247
|
+
3 < callback.params.length ||
|
|
238
248
|
callback.params.some(
|
|
239
249
|
(param) =>
|
|
240
250
|
param.type !== AST_NODE_TYPES.Identifier ||
|
|
@@ -266,14 +276,22 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
266
276
|
(variable) => variable.name === accumulatorParam.name,
|
|
267
277
|
);
|
|
268
278
|
|
|
269
|
-
if (accumulator === undefined)
|
|
279
|
+
if (accumulator === undefined) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
270
282
|
|
|
271
283
|
// `acc.at(-1)` is `T | undefined` only because `at` can go out of bounds,
|
|
272
284
|
// which a seeded accumulator never does. So once the value `scan` carries
|
|
273
285
|
// is known not to be nullish, a `?? fallback` guarding the read is dead
|
|
274
286
|
// and the fix takes it with the read — leaving it would hand the author an
|
|
275
|
-
// unnecessary condition that no rule can fix for them.
|
|
276
|
-
|
|
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
|
+
);
|
|
277
295
|
|
|
278
296
|
const mut_lastReads: Splice[] = [];
|
|
279
297
|
|
|
@@ -281,13 +299,19 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
281
299
|
const identifier = reference.identifier;
|
|
282
300
|
|
|
283
301
|
// The `...acc` / `Arr.toPushed(acc, …)` target the rewrite drops.
|
|
284
|
-
if (identifier === accumulatorRef)
|
|
302
|
+
if (identifier === accumulatorRef) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
285
305
|
|
|
286
|
-
if (identifier.type !== AST_NODE_TYPES.Identifier)
|
|
306
|
+
if (identifier.type !== AST_NODE_TYPES.Identifier) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
287
309
|
|
|
288
310
|
const lastRead = enclosingLastRead(identifier);
|
|
289
311
|
|
|
290
|
-
if (lastRead === undefined)
|
|
312
|
+
if (lastRead === undefined) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
291
315
|
|
|
292
316
|
const guard = carriesNullish ? undefined : nullishGuardAround(lastRead);
|
|
293
317
|
|
|
@@ -299,11 +323,15 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
299
323
|
|
|
300
324
|
// Two reads inside one `?? ` — `acc.at(-1) ?? acc.at(-1)` — would give
|
|
301
325
|
// ranges that contain one another, which no ordering of splices resolves.
|
|
302
|
-
if (overlaps(mut_lastReads))
|
|
326
|
+
if (overlaps(mut_lastReads)) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
303
329
|
|
|
304
330
|
// With no read of the previous value this is a `map` with a seed rather
|
|
305
331
|
// than a scan, and the accumulator parameter would be left unused.
|
|
306
|
-
if (mut_lastReads.length === 0)
|
|
332
|
+
if (mut_lastReads.length === 0) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
307
335
|
|
|
308
336
|
const parameterText = callback.params
|
|
309
337
|
.map((param) => sourceCode.getText(param))
|
|
@@ -315,12 +343,22 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
315
343
|
mut_lastReads,
|
|
316
344
|
);
|
|
317
345
|
|
|
318
|
-
const arrayText =
|
|
346
|
+
const arrayText = toArgumentText(
|
|
319
347
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
320
348
|
(call.callee as TSESTree.MemberExpression).object,
|
|
349
|
+
sourceCode,
|
|
321
350
|
);
|
|
322
351
|
|
|
323
|
-
|
|
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)})`;
|
|
324
362
|
};
|
|
325
363
|
|
|
326
364
|
/**
|
|
@@ -333,7 +371,9 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
333
371
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
334
372
|
callback: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
|
|
335
373
|
): readonly TSESTree.Node[] | undefined => {
|
|
336
|
-
if (!Arr.isFixedLengthArray(2, callback.params))
|
|
374
|
+
if (!Arr.isFixedLengthArray(2, callback.params)) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
337
377
|
|
|
338
378
|
const [elementParam, indexParam] = callback.params;
|
|
339
379
|
|
|
@@ -358,7 +398,9 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
358
398
|
return undefined;
|
|
359
399
|
}
|
|
360
400
|
|
|
361
|
-
if (elementVariable.references.length > 0)
|
|
401
|
+
if (elementVariable.references.length > 0) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
362
404
|
|
|
363
405
|
return indexVariable.references.map(
|
|
364
406
|
(reference) => reference.identifier as TSESTree.Node,
|
|
@@ -377,7 +419,11 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
377
419
|
|
|
378
420
|
const methodName = node.callee.property.name;
|
|
379
421
|
|
|
380
|
-
|
|
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
|
+
);
|
|
381
427
|
|
|
382
428
|
if (SLICING_METHODS.has(methodName)) {
|
|
383
429
|
mut_slices.push({ call: node, arrayText });
|
|
@@ -395,11 +441,15 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
395
441
|
|
|
396
442
|
switch (methodName) {
|
|
397
443
|
case 'map': {
|
|
398
|
-
if (rest.length > 0)
|
|
444
|
+
if (rest.length > 0) {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
399
447
|
|
|
400
448
|
const indexReads = indexReadsOf(callback);
|
|
401
449
|
|
|
402
|
-
if (indexReads === undefined)
|
|
450
|
+
if (indexReads === undefined) {
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
403
453
|
|
|
404
454
|
mut_candidates.push({
|
|
405
455
|
call: node,
|
|
@@ -415,7 +465,9 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
415
465
|
// Two arguments, so that the initial value is explicit: a `reduce`
|
|
416
466
|
// without one starts from the first element and has no `scan`
|
|
417
467
|
// spelling at all.
|
|
418
|
-
if (rest.length !== 1 || !appendsToItsAccumulator(callback))
|
|
468
|
+
if (rest.length !== 1 || !appendsToItsAccumulator(callback)) {
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
419
471
|
|
|
420
472
|
const [initial] = rest;
|
|
421
473
|
|
|
@@ -483,7 +535,9 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
483
535
|
),
|
|
484
536
|
);
|
|
485
537
|
|
|
486
|
-
if (rebuild === undefined)
|
|
538
|
+
if (rebuild === undefined) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
487
541
|
|
|
488
542
|
context.report({
|
|
489
543
|
node: candidate.call,
|
|
@@ -605,7 +659,9 @@ const appendsToItsAccumulator = (
|
|
|
605
659
|
): boolean => {
|
|
606
660
|
const [accumulatorParam] = callback.params;
|
|
607
661
|
|
|
608
|
-
if (accumulatorParam?.type !== AST_NODE_TYPES.Identifier)
|
|
662
|
+
if (accumulatorParam?.type !== AST_NODE_TYPES.Identifier) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
609
665
|
|
|
610
666
|
const { body } = callback;
|
|
611
667
|
|
|
@@ -621,9 +677,13 @@ const appendsToItsAccumulator = (
|
|
|
621
677
|
: undefined;
|
|
622
678
|
|
|
623
679
|
const target =
|
|
624
|
-
spelling
|
|
625
|
-
?
|
|
626
|
-
:
|
|
680
|
+
spelling === undefined || spelling === null
|
|
681
|
+
? undefined
|
|
682
|
+
: skipTypeWrappers(
|
|
683
|
+
spelling.type === AST_NODE_TYPES.SpreadElement
|
|
684
|
+
? spelling.argument
|
|
685
|
+
: spelling,
|
|
686
|
+
);
|
|
627
687
|
|
|
628
688
|
return (
|
|
629
689
|
target?.type === AST_NODE_TYPES.Identifier &&
|
|
@@ -645,24 +705,32 @@ const accumulationOf = (
|
|
|
645
705
|
): Accumulation | undefined => {
|
|
646
706
|
const [accumulatorParam] = callback.params;
|
|
647
707
|
|
|
648
|
-
if (accumulatorParam?.type !== AST_NODE_TYPES.Identifier)
|
|
708
|
+
if (accumulatorParam?.type !== AST_NODE_TYPES.Identifier) {
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
649
711
|
|
|
650
712
|
const { body } = callback;
|
|
651
713
|
|
|
652
714
|
// `[...acc, ELEMENT]` — exactly one spread and one new value, since `scan`
|
|
653
715
|
// appends exactly one per step.
|
|
654
716
|
if (body.type === AST_NODE_TYPES.ArrayExpression) {
|
|
655
|
-
if (!Arr.isFixedLengthArray(2, body.elements))
|
|
717
|
+
if (!Arr.isFixedLengthArray(2, body.elements)) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
656
720
|
|
|
657
721
|
const [spread, element] = body.elements;
|
|
658
722
|
|
|
723
|
+
const spreadTarget =
|
|
724
|
+
spread?.type === AST_NODE_TYPES.SpreadElement
|
|
725
|
+
? skipTypeWrappers(spread.argument)
|
|
726
|
+
: undefined;
|
|
727
|
+
|
|
659
728
|
return element === null ||
|
|
660
729
|
element.type === AST_NODE_TYPES.SpreadElement ||
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
spread.argument.name !== accumulatorParam.name
|
|
730
|
+
spreadTarget?.type !== AST_NODE_TYPES.Identifier ||
|
|
731
|
+
spreadTarget.name !== accumulatorParam.name
|
|
664
732
|
? undefined
|
|
665
|
-
: { accumulatorParam, accumulatorRef:
|
|
733
|
+
: { accumulatorParam, accumulatorRef: spreadTarget, element };
|
|
666
734
|
}
|
|
667
735
|
|
|
668
736
|
// `Arr.toPushed(acc, ELEMENT)`, matched on the method name as the rest of
|
|
@@ -680,7 +748,9 @@ const accumulationOf = (
|
|
|
680
748
|
return undefined;
|
|
681
749
|
}
|
|
682
750
|
|
|
683
|
-
const [
|
|
751
|
+
const [wrappedTarget, element] = body.arguments;
|
|
752
|
+
|
|
753
|
+
const target = skipTypeWrappers(wrappedTarget);
|
|
684
754
|
|
|
685
755
|
return element.type === AST_NODE_TYPES.SpreadElement ||
|
|
686
756
|
target.type !== AST_NODE_TYPES.Identifier ||
|