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
@@ -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
  ) {
@@ -67,7 +74,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
67
74
 
68
75
  const reducer = node.arguments[0];
69
76
 
70
- 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]);
71
81
 
72
82
  // Check initial value is 0
73
83
  if (
@@ -102,25 +112,43 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
102
112
  return;
103
113
  }
104
114
 
105
- // Case 1: (a, b) => a + b
106
115
  if (
107
- body.type === AST_NODE_TYPES.BinaryExpression &&
108
- body.operator === '+' &&
109
- body.left.type === AST_NODE_TYPES.Identifier &&
110
- body.left.name === param1.name &&
111
- body.right.type === AST_NODE_TYPES.Identifier &&
112
- body.right.name === param2.name
116
+ body.type !== AST_NODE_TYPES.BinaryExpression ||
117
+ body.operator !== '+'
113
118
  ) {
114
- // Check if arrayExpression type is number[] or compatible
119
+ return;
120
+ }
115
121
 
116
- const type = pipe(
117
- services?.esTreeNodeToTSNodeMap?.get(arrayExpression),
118
- ).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
+ }
119
151
 
120
- if (
121
- type !== undefined && // Check if it's an array type or tuple type
122
- (checker.isArrayType(type) || checker.isTupleType(type))
123
- ) {
124
152
  // Get type arguments using the official API
125
153
  // TypeReference has typeArguments that we can access
126
154
  const typeArguments = checker.getTypeArguments(
@@ -128,20 +156,22 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
128
156
  type as TypeReference,
129
157
  );
130
158
 
131
- if (Arr.isNonEmpty(typeArguments)) {
132
- const elementType = typeArguments[0];
133
-
134
- const numberType = checker.getNumberType();
135
-
136
- // Check if element type is assignable to number
137
- if (checker.isTypeAssignableTo(elementType, numberType)) {
138
- mut_nodesToFix.push({
139
- node,
140
- arrayExpression,
141
- messageId: 'useArrSum',
142
- });
143
- }
144
- }
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
+ });
145
175
  }
146
176
 
147
177
  return;
@@ -149,14 +179,12 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
149
179
 
150
180
  // Case 2: (a, b) => a.prop + b.prop or a['prop'] + b['prop']
151
181
  if (
152
- body.type === AST_NODE_TYPES.BinaryExpression &&
153
- body.operator === '+' &&
154
- body.left.type === AST_NODE_TYPES.MemberExpression &&
155
- body.right.type === AST_NODE_TYPES.MemberExpression
182
+ left.type === AST_NODE_TYPES.MemberExpression &&
183
+ right.type === AST_NODE_TYPES.MemberExpression
156
184
  ) {
157
- const leftObj = body.left.object;
185
+ const leftObj = skipTypeWrappers(left.object);
158
186
 
159
- const rightObj = body.right.object;
187
+ const rightObj = skipTypeWrappers(right.object);
160
188
 
161
189
  if (
162
190
  leftObj.type !== AST_NODE_TYPES.Identifier ||
@@ -168,25 +196,25 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
168
196
  }
169
197
 
170
198
  // Check if both access the same property
171
- const leftProp = body.left.property;
199
+ const leftProp = left.property;
172
200
 
173
- const rightProp = body.right.property;
201
+ const rightProp = right.property;
174
202
 
175
203
  let mut_propName: string | undefined;
176
204
 
177
205
  if (
178
206
  leftProp.type === AST_NODE_TYPES.Identifier &&
179
- !body.left.computed &&
207
+ !left.computed &&
180
208
  rightProp.type === AST_NODE_TYPES.Identifier &&
181
- !body.right.computed &&
209
+ !right.computed &&
182
210
  leftProp.name === rightProp.name
183
211
  ) {
184
212
  mut_propName = leftProp.name;
185
213
  } else if (
186
214
  leftProp.type === AST_NODE_TYPES.Literal &&
187
- body.left.computed &&
215
+ left.computed &&
188
216
  rightProp.type === AST_NODE_TYPES.Literal &&
189
- body.right.computed &&
217
+ right.computed &&
190
218
  leftProp.value === rightProp.value &&
191
219
  typeof leftProp.value === 'string'
192
220
  ) {
@@ -199,9 +227,9 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
199
227
 
200
228
  // Check if property type is number
201
229
  if (services?.program !== undefined && services.program !== null) {
202
- const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(body.left);
230
+ const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(left);
203
231
 
204
- const tsRightProp = services.esTreeNodeToTSNodeMap?.get(body.right);
232
+ const tsRightProp = services.esTreeNodeToTSNodeMap?.get(right);
205
233
 
206
234
  if (tsLeftProp !== undefined && tsRightProp !== undefined) {
207
235
  const leftPropType = checker.getTypeAtLocation(tsLeftProp);
@@ -223,7 +251,7 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
223
251
  // Generate mapper function
224
252
  const mapperParam = param1.name;
225
253
 
226
- const mapper = body.left.computed
254
+ const mapper = left.computed
227
255
  ? `${mapperParam} => ${mapperParam}['${mut_propName}']`
228
256
  : `${mapperParam} => ${mapperParam}.${mut_propName}`;
229
257
 
@@ -241,7 +269,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
241
269
  const hasArrImport = namedImports.includes('Arr');
242
270
 
243
271
  for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
244
- const arrayText = sourceCode.getText(nodeInfo.arrayExpression);
272
+ const arrayText = toArgumentText(
273
+ nodeInfo.arrayExpression,
274
+ sourceCode,
275
+ );
245
276
 
246
277
  context.report({
247
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);
@@ -5,6 +5,7 @@ import {
5
5
  } from '@typescript-eslint/utils';
6
6
  import { Arr } from 'ts-data-forge';
7
7
  import * as ts from 'typescript';
8
+ import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
8
9
  import {
9
10
  buildImportFixes,
10
11
  getImportedLocalName,
@@ -87,15 +88,19 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
87
88
  return;
88
89
  }
89
90
 
90
- const arrayTsNode = getTsNode(arrayExpression);
91
+ // At every layer of type wrappers (see typeWrapperLayers).
92
+ const isPrimitiveArray = typeWrapperLayers(arrayExpression).every(
93
+ (layer) => {
94
+ const tsNode = getTsNode(layer);
91
95
 
92
- if (arrayTsNode === undefined) {
93
- return;
94
- }
96
+ return (
97
+ tsNode !== undefined &&
98
+ isPrimitiveArrayType(checker, checker.getTypeAtLocation(tsNode))
99
+ );
100
+ },
101
+ );
95
102
 
96
- if (
97
- !isPrimitiveArrayType(checker, checker.getTypeAtLocation(arrayTsNode))
98
- ) {
103
+ if (!isPrimitiveArray) {
99
104
  return;
100
105
  }
101
106
 
@@ -221,8 +226,11 @@ const mutatingMethods: ReadonlySet<string> = new Set([
221
226
  */
222
227
  const getSetSourceArray = (
223
228
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
224
- node: TSESTree.Node,
229
+ wrapped: TSESTree.Node,
225
230
  ): TSESTree.Expression | undefined => {
231
+ // `new Set(xs) as ReadonlySet<string>` is still `new Set(xs)`.
232
+ const node = skipTypeWrappers(wrapped);
233
+
226
234
  if (
227
235
  node.type !== AST_NODE_TYPES.NewExpression ||
228
236
  node.callee.type !== AST_NODE_TYPES.Identifier ||
@@ -348,15 +356,17 @@ const needsMutableResult = (
348
356
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
349
357
  node: TSESTree.Node,
350
358
  ): boolean => {
351
- if (isUsedAsMutable(checker, getTsNode, node)) {
359
+ const outer = outermostTypePreservingWrapper(node);
360
+
361
+ if (isUsedAsMutable(checker, getTsNode, outer)) {
352
362
  return true;
353
363
  }
354
364
 
355
- const parent = node.parent;
365
+ const parent = outer.parent;
356
366
 
357
367
  if (
358
368
  parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
359
- parent.init !== node ||
369
+ parent.init !== outer ||
360
370
  parent.id.type !== AST_NODE_TYPES.Identifier ||
361
371
  parent.id.typeAnnotation !== undefined
362
372
  ) {
@@ -369,11 +379,35 @@ const needsMutableResult = (
369
379
  variable?.references.some(
370
380
  (reference) =>
371
381
  reference.init !== true &&
372
- isUsedAsMutable(checker, getTsNode, reference.identifier),
382
+ isUsedAsMutable(
383
+ checker,
384
+ getTsNode,
385
+ outermostTypePreservingWrapper(reference.identifier),
386
+ ),
373
387
  ) ?? false
374
388
  );
375
389
  };
376
390
 
391
+ /**
392
+ * `node`, or the outermost `!` / `satisfies` around it. Neither changes the
393
+ * type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
394
+ * `ys!.push(4)` still need a mutable array. An `as` does change it, and a
395
+ * readonly array may be cast to a mutable one, so the walk stops there.
396
+ */
397
+ const outermostTypePreservingWrapper = (
398
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
399
+ node: TSESTree.Node,
400
+ ): TSESTree.Node => {
401
+ const { parent } = node;
402
+
403
+ return parent !== undefined &&
404
+ (parent.type === AST_NODE_TYPES.TSNonNullExpression ||
405
+ parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
406
+ parent.expression === node
407
+ ? outermostTypePreservingWrapper(parent)
408
+ : node;
409
+ };
410
+
377
411
  const isUsedAsMutable = (
378
412
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
379
413
  checker: ts.TypeChecker,
@@ -453,3 +453,74 @@ describe('prefer-arr-uniq', () => {
453
453
  ],
454
454
  });
455
455
  });
456
+
457
+ describe('prefer-arr-uniq through type wrappers', () => {
458
+ tester.run('prefer-arr-uniq', preferArrUniq, {
459
+ valid: [],
460
+ invalid: [
461
+ {
462
+ name: 'a Set wrapped in `as` or `satisfies`',
463
+ code: dedent`
464
+ declare const xs: readonly string[];
465
+ const a = Array.from(new Set(xs) as ReadonlySet<string>);
466
+ const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
467
+ `,
468
+ output: dedent`
469
+ import { Arr } from 'ts-data-forge';
470
+ declare const xs: readonly string[];
471
+ const a = Arr.uniq(xs);
472
+ const b = Arr.uniq(xs);
473
+ `,
474
+ errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
475
+ },
476
+ {
477
+ name: 'only suggests when the result is mutated through `!`',
478
+ code: dedent`
479
+ const xs = [3, 1, 3];
480
+ Array.from(new Set(xs))!.push(4);
481
+ `,
482
+ output: null,
483
+ errors: [
484
+ {
485
+ messageId: 'useArrUniq',
486
+ suggestions: [
487
+ {
488
+ messageId: 'suggestArrUniq',
489
+ output: dedent`
490
+ import { Arr } from 'ts-data-forge';
491
+ const xs = [3, 1, 3];
492
+ Arr.uniq(xs)!.push(4);
493
+ `,
494
+ },
495
+ ],
496
+ },
497
+ ],
498
+ },
499
+ {
500
+ name: 'only suggests when a variable initialized through `!` is mutated',
501
+ code: dedent`
502
+ const xs = [3, 1, 3];
503
+ const ys = Array.from(new Set(xs))!;
504
+ ys.push(4);
505
+ `,
506
+ output: null,
507
+ errors: [
508
+ {
509
+ messageId: 'useArrUniq',
510
+ suggestions: [
511
+ {
512
+ messageId: 'suggestArrUniq',
513
+ output: dedent`
514
+ import { Arr } from 'ts-data-forge';
515
+ const xs = [3, 1, 3];
516
+ const ys = Arr.uniq(xs)!;
517
+ ys.push(4);
518
+ `,
519
+ },
520
+ ],
521
+ },
522
+ ],
523
+ },
524
+ ],
525
+ });
526
+ });
@@ -3,7 +3,8 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { type DeepReadonly } from 'ts-type-forge';
6
+ import type { DeepReadonly } from 'ts-type-forge';
7
+ import { toArgumentText } from './ast-utils.mjs';
7
8
  import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
8
9
  import {
9
10
  buildImportFixes,
@@ -38,25 +39,32 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
38
39
  const tsDataForgeImport = getTsDataForgeImport(program);
39
40
 
40
41
  const mut_nodesToFix: {
41
- node: TSESTree.TSAsExpression;
42
+ node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
42
43
  typeName: string;
43
44
  functionName: string;
44
45
  }[] = [];
45
46
 
46
- return {
47
- TSAsExpression: (node) => {
48
- const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
49
-
50
- if (typeInfo === undefined) {
51
- return;
52
- }
47
+ // `x as Int` and `<Int>x` are the same assertion.
48
+ const collect = (
49
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
50
+ node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
51
+ ): void => {
52
+ const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
53
+
54
+ if (typeInfo === undefined) {
55
+ return;
56
+ }
57
+
58
+ mut_nodesToFix.push({
59
+ node,
60
+ typeName: typeInfo.typeName,
61
+ functionName: typeInfo.functionName,
62
+ });
63
+ };
53
64
 
54
- mut_nodesToFix.push({
55
- node,
56
- typeName: typeInfo.typeName,
57
- functionName: typeInfo.functionName,
58
- });
59
- },
65
+ return {
66
+ TSAsExpression: collect,
67
+ TSTypeAssertion: collect,
60
68
  'Program:exit': () => {
61
69
  const namedImports = getNamedImports(tsDataForgeImport);
62
70
 
@@ -79,7 +87,7 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
79
87
  functionName,
80
88
  },
81
89
  fix: (fixer) => {
82
- const replacement = `${functionName}(${sourceCode.getText(node.expression)})`;
90
+ const replacement = `${functionName}(${toArgumentText(node.expression, sourceCode)})`;
83
91
 
84
92
  // Add import only for the first node of this function and only if not already imported
85
93
  const importFixes =
@@ -265,3 +265,54 @@ describe('prefer-as-int', () => {
265
265
  ],
266
266
  });
267
267
  }, 20000);
268
+
269
+ describe('prefer-as-int through type wrappers', () => {
270
+ tester.run('prefer-as-int', preferAsInt, {
271
+ valid: [],
272
+ invalid: [
273
+ {
274
+ name: 'an angle-bracket assertion',
275
+ code: dedent`
276
+ import { asInt } from 'ts-data-forge';
277
+ declare const n: number;
278
+ const a = <Int>n;
279
+ `,
280
+ output: dedent`
281
+ import { asInt } from 'ts-data-forge';
282
+ declare const n: number;
283
+ const a = asInt(n);
284
+ `,
285
+ errors: [{ messageId: 'useBrandedNumberCastFunction' }],
286
+ },
287
+ ],
288
+ });
289
+ }, 20000);
290
+
291
+ describe('prefer-as-int with parenthesized operands', () => {
292
+ tester.run('prefer-as-int', preferAsInt, {
293
+ valid: [],
294
+ invalid: [
295
+ {
296
+ name: 'a sequence operand keeps its parentheses',
297
+ code: dedent`
298
+ import { asInt } from 'ts-data-forge';
299
+ declare const log: () => void;
300
+ declare const n: number;
301
+ const a = (log(), n) as Int;
302
+ const b = <Int>(log(), n);
303
+ `,
304
+ output: dedent`
305
+ import { asInt } from 'ts-data-forge';
306
+ declare const log: () => void;
307
+ declare const n: number;
308
+ const a = asInt((log(), n));
309
+ const b = asInt((log(), n));
310
+ `,
311
+ errors: [
312
+ { messageId: 'useBrandedNumberCastFunction' },
313
+ { messageId: 'useBrandedNumberCastFunction' },
314
+ ],
315
+ },
316
+ ],
317
+ });
318
+ }, 20000);