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
@@ -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
+ });
@@ -4,7 +4,12 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import { Arr, pipe } from 'ts-data-forge';
7
- import { type TypeReference } from 'typescript';
7
+ import type { TypeReference } from 'typescript';
8
+ import {
9
+ skipTypeWrappers,
10
+ toArgumentText,
11
+ typeWrapperLayers,
12
+ } from './ast-utils.mjs';
8
13
  import {
9
14
  buildImportFixes,
10
15
  getNamedImports,
@@ -50,8 +55,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
50
55
  return {
51
56
  CallExpression: (node) => {
52
57
  // Check for xs.reduce(...)
58
+ // (`xs[reduce]` calls whatever method the variable `reduce` names)
53
59
  if (
54
60
  node.callee.type !== AST_NODE_TYPES.MemberExpression ||
61
+ node.callee.computed ||
55
62
  node.callee.property.type !== AST_NODE_TYPES.Identifier ||
56
63
  node.callee.property.name !== 'reduce'
57
64
  ) {
@@ -61,11 +68,16 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
61
68
  const arrayExpression = node.callee.object;
62
69
 
63
70
  // Check if we have 2 arguments: reducer function and initial value 0
64
- if (!Arr.isFixedLengthTuple(2, node.arguments)) return;
71
+ if (!Arr.isFixedLengthTuple(2, node.arguments)) {
72
+ return;
73
+ }
65
74
 
66
75
  const reducer = node.arguments[0];
67
76
 
68
- const initialValue = node.arguments[1];
77
+ // The initial value and the operands of the body are read through
78
+ // type wrappers: `0 as number` is still 0, `(b satisfies number)` is
79
+ // still `b`. Types are asked of the values underneath too.
80
+ const initialValue = skipTypeWrappers(node.arguments[1]);
69
81
 
70
82
  // Check initial value is 0
71
83
  if (
@@ -96,27 +108,47 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
96
108
 
97
109
  const checker = services?.program?.getTypeChecker();
98
110
 
99
- if (checker === undefined) return;
111
+ if (checker === undefined) {
112
+ return;
113
+ }
100
114
 
101
- // Case 1: (a, b) => a + b
102
115
  if (
103
- body.type === AST_NODE_TYPES.BinaryExpression &&
104
- body.operator === '+' &&
105
- body.left.type === AST_NODE_TYPES.Identifier &&
106
- body.left.name === param1.name &&
107
- body.right.type === AST_NODE_TYPES.Identifier &&
108
- body.right.name === param2.name
116
+ body.type !== AST_NODE_TYPES.BinaryExpression ||
117
+ body.operator !== '+'
109
118
  ) {
110
- // Check if arrayExpression type is number[] or compatible
119
+ return;
120
+ }
111
121
 
112
- const type = pipe(
113
- services?.esTreeNodeToTSNodeMap?.get(arrayExpression),
114
- ).mapNullable((tsNode) => checker.getTypeAtLocation(tsNode)).value;
122
+ const left = skipTypeWrappers(body.left);
123
+
124
+ const right = skipTypeWrappers(body.right);
125
+
126
+ // Case 1: (a, b) => a + b
127
+ if (
128
+ left.type === AST_NODE_TYPES.Identifier &&
129
+ left.name === param1.name &&
130
+ right.type === AST_NODE_TYPES.Identifier &&
131
+ right.name === param2.name
132
+ ) {
133
+ // Check if arrayExpression type is number[] or compatible at every
134
+ // layer of type wrappers: a cast does not turn strings, which `+`
135
+ // concatenates, into numbers.
136
+ const isNumberArray = (
137
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
138
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
139
+ expression: TSESTree.Node,
140
+ ): boolean => {
141
+ const type = pipe(
142
+ services?.esTreeNodeToTSNodeMap?.get(expression),
143
+ ).mapNullable((tsNode) => checker.getTypeAtLocation(tsNode)).value;
144
+
145
+ if (
146
+ type === undefined || // Check if it's an array type or tuple type
147
+ (!checker.isArrayType(type) && !checker.isTupleType(type))
148
+ ) {
149
+ return false;
150
+ }
115
151
 
116
- if (
117
- type !== undefined && // Check if it's an array type or tuple type
118
- (checker.isArrayType(type) || checker.isTupleType(type))
119
- ) {
120
152
  // Get type arguments using the official API
121
153
  // TypeReference has typeArguments that we can access
122
154
  const typeArguments = checker.getTypeArguments(
@@ -124,20 +156,22 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
124
156
  type as TypeReference,
125
157
  );
126
158
 
127
- if (Arr.isNonEmpty(typeArguments)) {
128
- const elementType = typeArguments[0];
129
-
130
- const numberType = checker.getNumberType();
131
-
132
- // Check if element type is assignable to number
133
- if (checker.isTypeAssignableTo(elementType, numberType)) {
134
- mut_nodesToFix.push({
135
- node,
136
- arrayExpression,
137
- messageId: 'useArrSum',
138
- });
139
- }
140
- }
159
+ // Check if element type is assignable to number
160
+ return (
161
+ Arr.isNonEmpty(typeArguments) &&
162
+ checker.isTypeAssignableTo(
163
+ typeArguments[0],
164
+ checker.getNumberType(),
165
+ )
166
+ );
167
+ };
168
+
169
+ if (typeWrapperLayers(arrayExpression).every(isNumberArray)) {
170
+ mut_nodesToFix.push({
171
+ node,
172
+ arrayExpression,
173
+ messageId: 'useArrSum',
174
+ });
141
175
  }
142
176
 
143
177
  return;
@@ -145,14 +179,12 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
145
179
 
146
180
  // Case 2: (a, b) => a.prop + b.prop or a['prop'] + b['prop']
147
181
  if (
148
- body.type === AST_NODE_TYPES.BinaryExpression &&
149
- body.operator === '+' &&
150
- body.left.type === AST_NODE_TYPES.MemberExpression &&
151
- body.right.type === AST_NODE_TYPES.MemberExpression
182
+ left.type === AST_NODE_TYPES.MemberExpression &&
183
+ right.type === AST_NODE_TYPES.MemberExpression
152
184
  ) {
153
- const leftObj = body.left.object;
185
+ const leftObj = skipTypeWrappers(left.object);
154
186
 
155
- const rightObj = body.right.object;
187
+ const rightObj = skipTypeWrappers(right.object);
156
188
 
157
189
  if (
158
190
  leftObj.type !== AST_NODE_TYPES.Identifier ||
@@ -164,38 +196,40 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
164
196
  }
165
197
 
166
198
  // Check if both access the same property
167
- const leftProp = body.left.property;
199
+ const leftProp = left.property;
168
200
 
169
- const rightProp = body.right.property;
201
+ const rightProp = right.property;
170
202
 
171
203
  let mut_propName: string | undefined;
172
204
 
173
205
  if (
174
206
  leftProp.type === AST_NODE_TYPES.Identifier &&
175
- !body.left.computed &&
207
+ !left.computed &&
176
208
  rightProp.type === AST_NODE_TYPES.Identifier &&
177
- !body.right.computed &&
209
+ !right.computed &&
178
210
  leftProp.name === rightProp.name
179
211
  ) {
180
212
  mut_propName = leftProp.name;
181
213
  } else if (
182
214
  leftProp.type === AST_NODE_TYPES.Literal &&
183
- body.left.computed &&
215
+ left.computed &&
184
216
  rightProp.type === AST_NODE_TYPES.Literal &&
185
- body.right.computed &&
217
+ right.computed &&
186
218
  leftProp.value === rightProp.value &&
187
219
  typeof leftProp.value === 'string'
188
220
  ) {
189
221
  mut_propName = leftProp.value;
190
222
  }
191
223
 
192
- if (mut_propName === undefined) return;
224
+ if (mut_propName === undefined) {
225
+ return;
226
+ }
193
227
 
194
228
  // Check if property type is number
195
229
  if (services?.program !== undefined && services.program !== null) {
196
- const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(body.left);
230
+ const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(left);
197
231
 
198
- const tsRightProp = services.esTreeNodeToTSNodeMap?.get(body.right);
232
+ const tsRightProp = services.esTreeNodeToTSNodeMap?.get(right);
199
233
 
200
234
  if (tsLeftProp !== undefined && tsRightProp !== undefined) {
201
235
  const leftPropType = checker.getTypeAtLocation(tsLeftProp);
@@ -217,7 +251,7 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
217
251
  // Generate mapper function
218
252
  const mapperParam = param1.name;
219
253
 
220
- const mapper = body.left.computed
254
+ const mapper = left.computed
221
255
  ? `${mapperParam} => ${mapperParam}['${mut_propName}']`
222
256
  : `${mapperParam} => ${mapperParam}.${mut_propName}`;
223
257
 
@@ -235,7 +269,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
235
269
  const hasArrImport = namedImports.includes('Arr');
236
270
 
237
271
  for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
238
- const arrayText = sourceCode.getText(nodeInfo.arrayExpression);
272
+ const arrayText = toArgumentText(
273
+ nodeInfo.arrayExpression,
274
+ sourceCode,
275
+ );
239
276
 
240
277
  context.report({
241
278
  node: nodeInfo.node,
@@ -169,3 +169,100 @@ describe('prefer-arr-sum', () => {
169
169
  ],
170
170
  });
171
171
  }, 20000);
172
+
173
+ describe('prefer-arr-sum through type wrappers', () => {
174
+ tester.run('prefer-arr-sum', preferArrSum, {
175
+ valid: [
176
+ {
177
+ name: 'strings cast to numbers are concatenated, not summed',
178
+ code: dedent`
179
+ declare const xs: readonly string[];
180
+ const sum = (xs as unknown as readonly number[]).reduce(
181
+ (a, b) => a + b,
182
+ 0,
183
+ );
184
+ `,
185
+ },
186
+ {
187
+ name: 'a string property cast to number',
188
+ code: dedent`
189
+ declare const xs: readonly Readonly<{ n: string }>[];
190
+ const sum = xs.reduce(
191
+ (a, b) => (a.n as unknown as number) + (b.n as unknown as number),
192
+ 0,
193
+ );
194
+ `,
195
+ },
196
+ {
197
+ name: 'a computed `reduce` is some other method',
198
+ code: dedent`
199
+ declare const xs: readonly number[];
200
+ declare const reduce: 'reduce';
201
+ const sum = xs[reduce]((a, b) => a + b, 0);
202
+ `,
203
+ },
204
+ ],
205
+ invalid: [
206
+ {
207
+ name: 'the initial value or an operand wrapped',
208
+ code: dedent`
209
+ import { Arr } from 'ts-data-forge';
210
+ declare const xs: readonly number[];
211
+ const a = xs.reduce((a, b) => a + b, 0 as number);
212
+ const b = xs.reduce((a, b) => a + (b satisfies number), 0);
213
+ const c = xs!.reduce((a, b) => a! + b, 0);
214
+ `,
215
+ output: dedent`
216
+ import { Arr } from 'ts-data-forge';
217
+ declare const xs: readonly number[];
218
+ const a = Arr.sum(xs);
219
+ const b = Arr.sum(xs);
220
+ const c = Arr.sum(xs!);
221
+ `,
222
+ errors: [
223
+ { messageId: 'useArrSum' },
224
+ { messageId: 'useArrSum' },
225
+ { messageId: 'useArrSum' },
226
+ ],
227
+ },
228
+ {
229
+ name: 'a property read through wrappers',
230
+ code: dedent`
231
+ import { Arr } from 'ts-data-forge';
232
+ declare const xs: readonly Readonly<{ n: number }>[];
233
+ const sum = xs.reduce((a, b) => (a.n satisfies number) + b!.n, 0);
234
+ `,
235
+ output: dedent`
236
+ import { Arr } from 'ts-data-forge';
237
+ declare const xs: readonly Readonly<{ n: number }>[];
238
+ const sum = Arr.sumBy(xs, a => a.n);
239
+ `,
240
+ errors: [{ messageId: 'useArrSumBy' }],
241
+ },
242
+ ],
243
+ });
244
+ }, 20000);
245
+
246
+ describe('prefer-arr-sum with parenthesized operands', () => {
247
+ tester.run('prefer-arr-sum', preferArrSum, {
248
+ valid: [],
249
+ invalid: [
250
+ {
251
+ name: 'a sequence array keeps its parentheses',
252
+ code: dedent`
253
+ import { Arr } from 'ts-data-forge';
254
+ declare const xs: readonly number[];
255
+ declare const log: () => void;
256
+ const sum = (log(), xs).reduce((a, b) => a + b, 0);
257
+ `,
258
+ output: dedent`
259
+ import { Arr } from 'ts-data-forge';
260
+ declare const xs: readonly number[];
261
+ declare const log: () => void;
262
+ const sum = Arr.sum((log(), xs));
263
+ `,
264
+ errors: [{ messageId: 'useArrSum' }],
265
+ },
266
+ ],
267
+ });
268
+ }, 20000);