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.
Files changed (139) hide show
  1. package/dist/plugin.d.mts +1 -1
  2. package/dist/plugin.d.mts.map +1 -1
  3. package/dist/plugin.mjs +1 -0
  4. package/dist/plugin.mjs.map +1 -1
  5. package/dist/rule-types.d.mts +3 -3
  6. package/dist/rule-types.d.mts.map +1 -1
  7. package/dist/rules/ast-utils.d.mts +59 -3
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +142 -31
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/branded-number-types.mjs.map +1 -1
  12. package/dist/rules/import-utils.d.mts +1 -1
  13. package/dist/rules/import-utils.d.mts.map +1 -1
  14. package/dist/rules/import-utils.mjs +26 -13
  15. package/dist/rules/import-utils.mjs.map +1 -1
  16. package/dist/rules/no-side-effect-import.d.mts +1 -1
  17. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  19. package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
  20. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  21. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-is-array.mjs +25 -11
  23. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +11 -21
  27. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
  29. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +16 -28
  31. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
  33. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-max-length-array.mjs +16 -24
  35. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
  37. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-min-length-array.mjs +16 -24
  39. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -22
  42. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-scan.d.mts +6 -5
  44. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  45. package/dist/rules/prefer-arr-scan.mjs +68 -31
  46. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  47. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  48. package/dist/rules/prefer-arr-sum.mjs +57 -40
  49. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  50. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  51. package/dist/rules/prefer-arr-uniq.mjs +59 -30
  52. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  53. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  54. package/dist/rules/prefer-as-int.mjs +27 -24
  55. package/dist/rules/prefer-as-int.mjs.map +1 -1
  56. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  57. package/dist/rules/prefer-canonical-array-slicing.mjs +59 -28
  58. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  59. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  60. package/dist/rules/prefer-canonical-length-cast.mjs +41 -16
  61. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  62. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  63. package/dist/rules/prefer-canonical-length-guard.mjs +18 -8
  64. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  65. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  66. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +31 -6
  67. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  68. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  69. package/dist/rules/prefer-is-non-null-object.mjs +56 -40
  70. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  71. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  72. package/dist/rules/prefer-is-record-and-has-key.mjs +18 -8
  73. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  74. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  75. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
  77. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  78. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
  80. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  81. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  84. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.mjs +15 -8
  86. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  87. package/dist/types.d.mts +2 -2
  88. package/dist/types.d.mts.map +1 -1
  89. package/package.json +25 -23
  90. package/src/plugin.mts +5 -4
  91. package/src/plugin.test.mts +0 -13
  92. package/src/rule-types.mts +3 -3
  93. package/src/rules/ast-utils.mts +192 -37
  94. package/src/rules/branded-number-types.mts +1 -1
  95. package/src/rules/import-utils.mts +41 -17
  96. package/src/rules/no-side-effect-import.mts +1 -1
  97. package/src/rules/no-unnecessary-type-guard.mts +249 -148
  98. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  99. package/src/rules/prefer-arr-is-array.mts +32 -13
  100. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  101. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
  102. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  103. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  104. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  105. package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
  106. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  107. package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
  108. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  109. package/src/rules/prefer-arr-is-non-empty.mts +19 -24
  110. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  111. package/src/rules/prefer-arr-scan.mts +107 -37
  112. package/src/rules/prefer-arr-scan.test.mts +149 -0
  113. package/src/rules/prefer-arr-sum.mts +87 -50
  114. package/src/rules/prefer-arr-sum.test.mts +97 -0
  115. package/src/rules/prefer-arr-uniq.mts +83 -33
  116. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  117. package/src/rules/prefer-as-int.mts +38 -38
  118. package/src/rules/prefer-as-int.test.mts +51 -0
  119. package/src/rules/prefer-canonical-array-slicing.mts +84 -29
  120. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  121. package/src/rules/prefer-canonical-length-cast.mts +56 -18
  122. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  123. package/src/rules/prefer-canonical-length-guard.mts +25 -11
  124. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  125. package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
  126. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  127. package/src/rules/prefer-is-non-null-object.mts +57 -33
  128. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  129. package/src/rules/prefer-is-record-and-has-key.mts +27 -12
  130. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  131. package/src/rules/prefer-num-safe-parse-float.mts +37 -13
  132. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  133. package/src/rules/prefer-num-safe-parse-int.mts +41 -15
  134. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  135. package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
  136. package/src/rules/prefer-range-for-loop.mts +21 -9
  137. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  138. package/src/rules/rules.mts +1 -1
  139. 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 !== '<') return;
54
+ if (node.operator !== '>' && node.operator !== '<') {
55
+ return;
56
+ }
49
57
 
50
58
  const isLengthOnLeft = node.operator === '>';
51
59
 
52
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
60
+ // Both sides are read through type wrappers.
61
+ const lengthSide = asLengthAccess(
62
+ node[isLengthOnLeft ? 'left' : 'right'],
63
+ );
53
64
 
54
- const numberSide = node[isLengthOnLeft ? 'right' : 'left'];
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.type !== AST_NODE_TYPES.MemberExpression ||
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?.program !== undefined && services.program !== null) {
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 = sourceCode.getText(arrayExpression);
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 { type FixedLengthTuple } from 'ts-type-forge';
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 left in
139
- * place rather than removed: it evaluates identically either way, and deciding
140
- * that it is dead needs to know that `S` excludes `undefined`, which is type
141
- * information this rule does not take.
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) return true;
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) return undefined;
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 > 3 ||
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) return 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
- const carriesNullish = isNullish(seed) || isNullish(element);
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) continue;
302
+ if (identifier === accumulatorRef) {
303
+ continue;
304
+ }
285
305
 
286
- if (identifier.type !== AST_NODE_TYPES.Identifier) return undefined;
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) return 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)) return undefined;
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) return undefined;
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 = sourceCode.getText(
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
- return `Arr.scan(${arrayText}, (${parameterText}) => ${elementText}, ${sourceCode.getText(seed)})`;
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)) return undefined;
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) return undefined;
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
- const arrayText = sourceCode.getText(node.callee.object);
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) break;
444
+ if (rest.length > 0) {
445
+ break;
446
+ }
399
447
 
400
448
  const indexReads = indexReadsOf(callback);
401
449
 
402
- if (indexReads === undefined) break;
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)) break;
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) continue;
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) return false;
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?.type === AST_NODE_TYPES.SpreadElement
625
- ? spelling.argument
626
- : spelling;
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) return undefined;
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)) return undefined;
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
- spread?.type !== AST_NODE_TYPES.SpreadElement ||
662
- spread.argument.type !== AST_NODE_TYPES.Identifier ||
663
- spread.argument.name !== accumulatorParam.name
730
+ spreadTarget?.type !== AST_NODE_TYPES.Identifier ||
731
+ spreadTarget.name !== accumulatorParam.name
664
732
  ? undefined
665
- : { accumulatorParam, accumulatorRef: spread.argument, element };
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 [target, element] = body.arguments;
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 ||