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.
Files changed (132) 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 +119 -8
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/import-utils.d.mts +1 -1
  12. package/dist/rules/import-utils.d.mts.map +1 -1
  13. package/dist/rules/import-utils.mjs +22 -11
  14. package/dist/rules/import-utils.mjs.map +1 -1
  15. package/dist/rules/no-side-effect-import.d.mts +1 -1
  16. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  17. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.mjs +125 -79
  19. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-is-array.mjs +11 -4
  21. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  22. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
  23. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +5 -19
  25. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  26. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
  27. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +11 -27
  29. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  30. package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
  31. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -22
  33. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  34. package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
  35. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  36. package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -22
  37. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  38. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  39. package/dist/rules/prefer-arr-is-non-empty.mjs +7 -22
  40. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  41. package/dist/rules/prefer-arr-scan.d.mts +6 -5
  42. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  43. package/dist/rules/prefer-arr-scan.mjs +33 -13
  44. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  45. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  46. package/dist/rules/prefer-arr-sum.mjs +51 -37
  47. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  48. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  49. package/dist/rules/prefer-arr-uniq.mjs +33 -10
  50. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  51. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  52. package/dist/rules/prefer-as-int.mjs +18 -12
  53. package/dist/rules/prefer-as-int.mjs.map +1 -1
  54. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  55. package/dist/rules/prefer-canonical-array-slicing.mjs +29 -14
  56. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  57. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  58. package/dist/rules/prefer-canonical-length-cast.mjs +34 -13
  59. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  60. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  61. package/dist/rules/prefer-canonical-length-guard.mjs +10 -4
  62. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  63. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  64. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +25 -3
  65. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  66. package/dist/rules/prefer-is-non-null-object.mjs +28 -10
  67. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  68. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  69. package/dist/rules/prefer-is-record-and-has-key.mjs +8 -3
  70. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  71. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  72. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  73. package/dist/rules/prefer-num-safe-parse-float.mjs +18 -7
  74. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  75. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-int.mjs +15 -8
  77. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  78. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  79. package/dist/rules/prefer-range-for-loop.mjs +13 -7
  80. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  81. package/dist/types.d.mts +2 -2
  82. package/dist/types.d.mts.map +1 -1
  83. package/package.json +3 -3
  84. package/src/plugin.mts +5 -4
  85. package/src/plugin.test.mts +0 -13
  86. package/src/rule-types.mts +3 -3
  87. package/src/rules/ast-utils.mts +156 -13
  88. package/src/rules/import-utils.mts +35 -15
  89. package/src/rules/no-side-effect-import.mts +1 -1
  90. package/src/rules/no-unnecessary-type-guard.mts +192 -124
  91. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  92. package/src/rules/prefer-arr-is-array.mts +11 -6
  93. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  94. package/src/rules/prefer-arr-is-bounded-length-array.mts +13 -25
  95. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  96. package/src/rules/prefer-arr-is-fixed-length-array.mts +18 -38
  97. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  98. package/src/rules/prefer-arr-is-max-length-array.mts +15 -26
  99. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  100. package/src/rules/prefer-arr-is-min-length-array.mts +15 -26
  101. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  102. package/src/rules/prefer-arr-is-non-empty.mts +16 -25
  103. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  104. package/src/rules/prefer-arr-scan.mts +55 -19
  105. package/src/rules/prefer-arr-scan.test.mts +149 -0
  106. package/src/rules/prefer-arr-sum.mts +78 -47
  107. package/src/rules/prefer-arr-sum.test.mts +97 -0
  108. package/src/rules/prefer-arr-uniq.mts +46 -12
  109. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  110. package/src/rules/prefer-as-int.mts +24 -16
  111. package/src/rules/prefer-as-int.test.mts +51 -0
  112. package/src/rules/prefer-canonical-array-slicing.mts +38 -15
  113. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  114. package/src/rules/prefer-canonical-length-cast.mts +45 -15
  115. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  116. package/src/rules/prefer-canonical-length-guard.mts +13 -7
  117. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  118. package/src/rules/prefer-comparison-over-nullish-guard.mts +33 -6
  119. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  120. package/src/rules/prefer-is-non-null-object.mts +31 -11
  121. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  122. package/src/rules/prefer-is-record-and-has-key.mts +10 -5
  123. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  124. package/src/rules/prefer-num-safe-parse-float.mts +24 -11
  125. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  126. package/src/rules/prefer-num-safe-parse-int.mts +25 -11
  127. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  128. package/src/rules/prefer-obj-over-entries-round-trip.mts +1 -1
  129. package/src/rules/prefer-range-for-loop.mts +18 -8
  130. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  131. package/src/rules/rules.mts +1 -1
  132. package/src/types.mts +2 -2
@@ -1,8 +1,11 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
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
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
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 (!isLengthAccess(lengthSide)) {
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?.program !== undefined && services.program !== null) {
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
- lengthExpression: valueSide,
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 = sourceCode.getText(arrayExpression);
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
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
60
+ // Both sides are read through type wrappers.
61
+ const lengthSide = asLengthAccess(
62
+ node[isLengthOnLeft ? 'left' : 'right'],
63
+ );
55
64
 
56
- const numberSide = node[isLengthOnLeft ? 'right' : 'left'];
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.type !== AST_NODE_TYPES.MemberExpression ||
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?.program !== undefined && services.program !== null) {
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 = sourceCode.getText(arrayExpression);
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 { 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
  */
@@ -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
- 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
+ );
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 = sourceCode.getText(
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
- 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)})`;
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
- 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
+ );
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?.type === AST_NODE_TYPES.SpreadElement
655
- ? spelling.argument
656
- : spelling;
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
- spread?.type !== AST_NODE_TYPES.SpreadElement ||
696
- spread.argument.type !== AST_NODE_TYPES.Identifier ||
697
- spread.argument.name !== accumulatorParam.name
730
+ spreadTarget?.type !== AST_NODE_TYPES.Identifier ||
731
+ spreadTarget.name !== accumulatorParam.name
698
732
  ? undefined
699
- : { accumulatorParam, accumulatorRef: spread.argument, element };
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 [target, element] = body.arguments;
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
+ });