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
@@ -4,6 +4,7 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import * as ts from 'typescript';
7
+ import { toArgumentText, typeWrapperLayers } from './ast-utils.mjs';
7
8
  import {
8
9
  buildImportFixes,
9
10
  getNamedImports,
@@ -36,7 +37,9 @@ const isStringType = (type: ts.Type): boolean => {
36
37
  * `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` (ts-data-forge).
37
38
  *
38
39
  * `Number(x)` is only flagged when x is purely a string type to avoid false
39
- * positives for `Number(someBoolean)` or `Number(someNumber)` uses.
40
+ * positives for `Number(someBoolean)` or `Number(someNumber)` uses. The type is
41
+ * asked of the value under any `as` / `!` / `satisfies` too, so a cast does not
42
+ * vouch for a string that is not one.
40
43
  */
41
44
  export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
42
45
  {
@@ -91,32 +94,53 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
91
94
  callee.type === AST_NODE_TYPES.Identifier &&
92
95
  callee.name === 'Number';
93
96
 
94
- if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall)
97
+ if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
95
98
  return;
99
+ }
96
100
 
97
101
  const args = node.arguments;
98
102
 
99
103
  const firstArg = args[0];
100
104
 
101
- if (firstArg === undefined) return;
105
+ if (firstArg === undefined) {
106
+ return;
107
+ }
102
108
 
103
109
  // Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
104
- if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
110
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
111
+ return;
112
+ }
105
113
 
106
114
  // The argument must be purely `string` so the autofix is type-safe
107
115
  // against `Num.safeParseFloat(s: string)`. Without type information,
108
116
  // skip.
109
- if (services?.program == null) return;
117
+ if (services?.program == null) {
118
+ return;
119
+ }
110
120
 
111
121
  const checker = services.program.getTypeChecker();
112
122
 
113
- const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
114
-
115
- if (tsNode === undefined) return;
116
-
117
- const argType = checker.getTypeAtLocation(tsNode);
118
-
119
- if (!isStringType(argType)) return;
123
+ // Every layer of type wrappers must be `string` (see
124
+ // typeWrapperLayers): the value underneath is what `Number` sees at
125
+ // run time (`Number(v as string)` may still be handed `true`), and
126
+ // the type a wrapper states is what the fix, which keeps the
127
+ // wrapper, passes on.
128
+ const isString = (
129
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
130
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
131
+ expression: TSESTree.Node,
132
+ ): boolean => {
133
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(expression);
134
+
135
+ return (
136
+ tsNode !== undefined &&
137
+ isStringType(checker.getTypeAtLocation(tsNode))
138
+ );
139
+ };
140
+
141
+ if (!typeWrapperLayers(firstArg).every(isString)) {
142
+ return;
143
+ }
120
144
 
121
145
  mut_nodesToFix.push({ node, argExpression: firstArg });
122
146
  },
@@ -131,7 +155,7 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
131
155
  index,
132
156
  { node, argExpression },
133
157
  ] of mut_nodesToFix.entries()) {
134
- const argText = sourceCode.getText(argExpression);
158
+ const argText = toArgumentText(argExpression, sourceCode);
135
159
 
136
160
  const originalText = sourceCode.getText(node);
137
161
 
@@ -161,3 +161,82 @@ describe('prefer-num-safe-parse-float', () => {
161
161
  ],
162
162
  });
163
163
  }, 20000);
164
+
165
+ describe('prefer-num-safe-parse-float through type wrappers', () => {
166
+ tester.run('prefer-num-safe-parse-float', preferNumSafeParseFloat, {
167
+ valid: [
168
+ {
169
+ name: 'an unknown value cast to string',
170
+ code: dedent`
171
+ declare const v: unknown;
172
+ const a = Number(v as string);
173
+ const b = parseFloat(<string>v);
174
+ `,
175
+ },
176
+ {
177
+ name: 'a number cast to string',
178
+ code: dedent`
179
+ declare const n: number;
180
+ const a = Number(n as unknown as string);
181
+ `,
182
+ },
183
+ {
184
+ name: 'a string whose type a middle layer does not vouch for',
185
+ code: dedent`
186
+ declare const s: string;
187
+ const a = Number(s as unknown as string);
188
+ `,
189
+ },
190
+ {
191
+ name: 'a `!` that leaves null in place (`Number(null)` is 0)',
192
+ code: dedent`
193
+ declare const s: string | null;
194
+ const a = Number(s!);
195
+ `,
196
+ },
197
+ ],
198
+ invalid: [
199
+ {
200
+ name: 'a string argument keeps its wrapper',
201
+ code: dedent`
202
+ declare const s: string;
203
+ const a = Number(s!);
204
+ const b = parseFloat(s satisfies string);
205
+ `,
206
+ output: dedent`
207
+ import { Num, Result } from 'ts-data-forge';
208
+ declare const s: string;
209
+ const a = Result.unwrapOkOr(Num.safeParseFloat(s!), Number.NaN);
210
+ const b = Result.unwrapOkOr(Num.safeParseFloat(s satisfies string), Number.NaN);
211
+ `,
212
+ errors: [
213
+ { messageId: 'useSafeParseFloat' },
214
+ { messageId: 'useSafeParseFloat' },
215
+ ],
216
+ },
217
+ ],
218
+ });
219
+ }, 20000);
220
+
221
+ describe('prefer-num-safe-parse-float with parenthesized operands', () => {
222
+ tester.run('prefer-num-safe-parse-float', preferNumSafeParseFloat, {
223
+ valid: [],
224
+ invalid: [
225
+ {
226
+ name: 'a sequence argument keeps its parentheses',
227
+ code: dedent`
228
+ declare const s: string;
229
+ declare const log: () => void;
230
+ const a = Number((log(), s));
231
+ `,
232
+ output: dedent`
233
+ import { Num, Result } from 'ts-data-forge';
234
+ declare const s: string;
235
+ declare const log: () => void;
236
+ const a = Result.unwrapOkOr(Num.safeParseFloat((log(), s)), Number.NaN);
237
+ `,
238
+ errors: [{ messageId: 'useSafeParseFloat' }],
239
+ },
240
+ ],
241
+ });
242
+ }, 20000);
@@ -4,6 +4,11 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import * as ts from 'typescript';
7
+ import {
8
+ skipTypeWrappers,
9
+ toArgumentText,
10
+ typeWrapperLayers,
11
+ } from './ast-utils.mjs';
7
12
  import {
8
13
  buildImportFixes,
9
14
  getNamedImports,
@@ -84,20 +89,28 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
84
89
  callee.property.type === AST_NODE_TYPES.Identifier &&
85
90
  callee.property.name === 'parseInt';
86
91
 
87
- if (!isGlobalParseInt && !isNumberParseInt) return;
92
+ if (!isGlobalParseInt && !isNumberParseInt) {
93
+ return;
94
+ }
88
95
 
89
96
  const args = node.arguments;
90
97
 
91
98
  const firstArg = args[0];
92
99
 
93
- if (firstArg === undefined) return;
100
+ if (firstArg === undefined) {
101
+ return;
102
+ }
94
103
 
95
104
  // Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
96
- if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
105
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
106
+ return;
107
+ }
97
108
 
98
- // Only base 10: accept a missing radix or an explicit literal `10`.
109
+ // Only base 10: accept a missing radix or an explicit literal `10`,
110
+ // under any type wrapper (`10 as const`).
99
111
  if (args.length >= 2) {
100
- const radix = args[1];
112
+ const radix =
113
+ args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
101
114
 
102
115
  if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
103
116
  return;
@@ -105,18 +118,31 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
105
118
  }
106
119
 
107
120
  // The argument must be a `string` so the autofix is type-safe against
108
- // `Num.safeParseInt(s: string)`. Without type information, skip.
109
- if (services?.program == null) return;
121
+ // `Num.safeParseInt(s: string)`. Without type information, skip. Every
122
+ // layer of type wrappers is asked (see typeWrapperLayers): a cast
123
+ // does not make a number a string.
124
+ if (services?.program == null) {
125
+ return;
126
+ }
110
127
 
111
128
  const checker = services.program.getTypeChecker();
112
129
 
113
- const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
114
-
115
- if (tsNode === undefined) return;
116
-
117
- const argType = checker.getTypeAtLocation(tsNode);
118
-
119
- if (!isStringType(argType)) return;
130
+ const isString = (
131
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
132
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
133
+ expression: TSESTree.Node,
134
+ ): boolean => {
135
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(expression);
136
+
137
+ return (
138
+ tsNode !== undefined &&
139
+ isStringType(checker.getTypeAtLocation(tsNode))
140
+ );
141
+ };
142
+
143
+ if (!typeWrapperLayers(firstArg).every(isString)) {
144
+ return;
145
+ }
120
146
 
121
147
  mut_nodesToFix.push({ node, argExpression: firstArg });
122
148
  },
@@ -131,7 +157,7 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
131
157
  index,
132
158
  { node, argExpression },
133
159
  ] of mut_nodesToFix.entries()) {
134
- const argText = sourceCode.getText(argExpression);
160
+ const argText = toArgumentText(argExpression, sourceCode);
135
161
 
136
162
  const originalText = sourceCode.getText(node);
137
163
 
@@ -135,3 +135,67 @@ describe('prefer-num-safe-parse-int', () => {
135
135
  ],
136
136
  });
137
137
  }, 20000);
138
+
139
+ describe('prefer-num-safe-parse-int through type wrappers', () => {
140
+ tester.run('prefer-num-safe-parse-int', preferNumSafeParseInt, {
141
+ valid: [
142
+ {
143
+ name: 'a number cast to string',
144
+ code: dedent`
145
+ declare const n: number;
146
+ const a = parseInt(n as unknown as string, 10);
147
+ `,
148
+ },
149
+ {
150
+ name: 'a wrapped radix other than 10',
151
+ code: dedent`
152
+ declare const s: string;
153
+ const a = parseInt(s, 16 as const);
154
+ `,
155
+ },
156
+ ],
157
+ invalid: [
158
+ {
159
+ name: 'a radix wrapped in `as const` or `satisfies`',
160
+ code: dedent`
161
+ declare const s: string;
162
+ const a = parseInt(s, 10 as const);
163
+ const b = Number.parseInt(s!, 10 satisfies number);
164
+ `,
165
+ output: dedent`
166
+ import { Num, Result } from 'ts-data-forge';
167
+ declare const s: string;
168
+ const a = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
169
+ const b = Result.unwrapOkOr(Num.safeParseInt(s!), Number.NaN);
170
+ `,
171
+ errors: [
172
+ { messageId: 'useSafeParseInt' },
173
+ { messageId: 'useSafeParseInt' },
174
+ ],
175
+ },
176
+ ],
177
+ });
178
+ }, 20000);
179
+
180
+ describe('prefer-num-safe-parse-int with parenthesized operands', () => {
181
+ tester.run('prefer-num-safe-parse-int', preferNumSafeParseInt, {
182
+ valid: [],
183
+ invalid: [
184
+ {
185
+ name: 'a sequence argument keeps its parentheses',
186
+ code: dedent`
187
+ declare const s: string;
188
+ declare const log: () => void;
189
+ const a = parseInt((log(), s), 10);
190
+ `,
191
+ output: dedent`
192
+ import { Num, Result } from 'ts-data-forge';
193
+ declare const s: string;
194
+ declare const log: () => void;
195
+ const a = Result.unwrapOkOr(Num.safeParseInt((log(), s)), Number.NaN);
196
+ `,
197
+ errors: [{ messageId: 'useSafeParseInt' }],
198
+ },
199
+ ],
200
+ });
201
+ }, 20000);
@@ -4,7 +4,7 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import { Arr } from 'ts-data-forge';
7
- import { type ReadonlyRecord } from 'ts-type-forge';
7
+ import type { ReadonlyRecord } from 'ts-type-forge';
8
8
  import {
9
9
  buildImportFixes,
10
10
  getImportedLocalName,
@@ -159,7 +159,9 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
159
159
  CallExpression: (node) => {
160
160
  const roundTrip = parseEntriesRoundTrip(node);
161
161
 
162
- if (roundTrip === undefined) return;
162
+ if (roundTrip === undefined) {
163
+ return;
164
+ }
163
165
 
164
166
  mut_roundTrips.push({ node, roundTrip });
165
167
  },
@@ -225,13 +227,19 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
225
227
  const parseEntriesRoundTrip = (
226
228
  node: TSESTree.CallExpression,
227
229
  ): RoundTrip | undefined => {
228
- if (!isObjectStaticCall(node, 'fromEntries')) return undefined;
230
+ if (!isObjectStaticCall(node, 'fromEntries')) {
231
+ return undefined;
232
+ }
229
233
 
230
- if (!Arr.isFixedLengthTuple(1, node.arguments)) return undefined;
234
+ if (!Arr.isFixedLengthTuple(1, node.arguments)) {
235
+ return undefined;
236
+ }
231
237
 
232
238
  const [transform] = node.arguments;
233
239
 
234
- if (transform.type !== AST_NODE_TYPES.CallExpression) return undefined;
240
+ if (transform.type !== AST_NODE_TYPES.CallExpression) {
241
+ return undefined;
242
+ }
235
243
 
236
244
  const { callee } = transform;
237
245
 
@@ -265,11 +273,15 @@ const parseEntriesRoundTrip = (
265
273
 
266
274
  const [record] = entriesCall.arguments;
267
275
 
268
- if (record.type === AST_NODE_TYPES.SpreadElement) return undefined;
276
+ if (record.type === AST_NODE_TYPES.SpreadElement) {
277
+ return undefined;
278
+ }
269
279
 
270
280
  // A second argument (`thisArg`) or a callback that also takes the entry index
271
281
  // has no counterpart in `Obj`.
272
- if (!Arr.isFixedLengthTuple(1, transform.arguments)) return undefined;
282
+ if (!Arr.isFixedLengthTuple(1, transform.arguments)) {
283
+ return undefined;
284
+ }
273
285
 
274
286
  const [callback] = transform.arguments;
275
287
 
@@ -287,11 +299,15 @@ const parseEntriesRoundTrip = (
287
299
 
288
300
  const [entryPattern] = callback.params;
289
301
 
290
- if (entryPattern.type !== AST_NODE_TYPES.ArrayPattern) return undefined;
302
+ if (entryPattern.type !== AST_NODE_TYPES.ArrayPattern) {
303
+ return undefined;
304
+ }
291
305
 
292
306
  const entryParams = parseEntryPattern(entryPattern);
293
307
 
294
- if (entryParams === undefined) return undefined;
308
+ if (entryParams === undefined) {
309
+ return undefined;
310
+ }
295
311
 
296
312
  const body = callback.body;
297
313
 
@@ -320,7 +336,9 @@ const parseEntriesRoundTrip = (
320
336
  }
321
337
 
322
338
  case 'flatMap': {
323
- if (body.type !== AST_NODE_TYPES.ConditionalExpression) return undefined;
339
+ if (body.type !== AST_NODE_TYPES.ConditionalExpression) {
340
+ return undefined;
341
+ }
324
342
 
325
343
  const onTrue = parseSingletonEntry(body.consequent, entryParams.keyParam);
326
344
 
@@ -365,7 +383,9 @@ type EntryParams = Readonly<{
365
383
  const parseEntryPattern = (
366
384
  pattern: TSESTree.ArrayPattern,
367
385
  ): EntryParams | undefined => {
368
- if (pattern.elements.length > 2) return undefined;
386
+ if (pattern.elements.length > 2) {
387
+ return undefined;
388
+ }
369
389
 
370
390
  const [keyElement, valueElement] = pattern.elements;
371
391
 
@@ -399,7 +419,9 @@ const parseKeptEntry = (
399
419
  node: TSESTree.Expression,
400
420
  keyParam: TSESTree.Identifier | undefined,
401
421
  ): KeptEntry | undefined => {
402
- if (keyParam === undefined) return undefined;
422
+ if (keyParam === undefined) {
423
+ return undefined;
424
+ }
403
425
 
404
426
  const tuple = stripTypeWrappers(node);
405
427
 
@@ -4,6 +4,7 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import { Arr } from 'ts-data-forge';
7
+ import { skipTypeWrappers, toArgumentText } from './ast-utils.mjs';
7
8
  import {
8
9
  buildImportFixes,
9
10
  getNamedImports,
@@ -68,12 +69,21 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
68
69
 
69
70
  const beginExpr = declaration.init;
70
71
 
71
- // Check test: i < end
72
+ // Check test: i < end. The loop variable and the step are read
73
+ // through type wrappers: `(i satisfies number) < n` tests `i`, and
74
+ // `i += 0 as number` still steps by 0.
72
75
  if (
73
76
  node.test?.type !== AST_NODE_TYPES.BinaryExpression ||
74
- node.test.operator !== '<' ||
75
- node.test.left.type !== AST_NODE_TYPES.Identifier ||
76
- node.test.left.name !== varName
77
+ node.test.operator !== '<'
78
+ ) {
79
+ return;
80
+ }
81
+
82
+ const testedVariable = skipTypeWrappers(node.test.left);
83
+
84
+ if (
85
+ testedVariable.type !== AST_NODE_TYPES.Identifier ||
86
+ testedVariable.name !== varName
77
87
  ) {
78
88
  return;
79
89
  }
@@ -81,7 +91,9 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
81
91
  const endExpr = node.test.right;
82
92
 
83
93
  // Check update: ++i, i++, i += 1, or i += step
84
- if (node.update === null) return;
94
+ if (node.update === null) {
95
+ return;
96
+ }
85
97
 
86
98
  let mut_step: string | undefined;
87
99
 
@@ -103,7 +115,7 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
103
115
  node.update.left.name === varName
104
116
  ) {
105
117
  // i += step
106
- const stepValue = node.update.right;
118
+ const stepValue = skipTypeWrappers(node.update.right);
107
119
 
108
120
  if (stepValue.type === AST_NODE_TYPES.Literal) {
109
121
  const stepNum = stepValue.value;
@@ -116,15 +128,15 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
116
128
  }
117
129
  } else {
118
130
  // Variable step like i += step
119
- mut_step = sourceCode.getText(stepValue);
131
+ mut_step = toArgumentText(node.update.right, sourceCode);
120
132
  }
121
133
  } else {
122
134
  return;
123
135
  }
124
136
 
125
- const beginText = sourceCode.getText(beginExpr);
137
+ const beginText = toArgumentText(beginExpr, sourceCode);
126
138
 
127
- const endText = sourceCode.getText(endExpr);
139
+ const endText = toArgumentText(endExpr, sourceCode);
128
140
 
129
141
  mut_nodesToFix.push({
130
142
  node,
@@ -154,3 +154,78 @@ describe('prefer-range-for-loop', () => {
154
154
  ],
155
155
  });
156
156
  }, 20000);
157
+
158
+ describe('prefer-range-for-loop through type wrappers', () => {
159
+ tester.run('prefer-range-for-loop', preferRangeForLoop, {
160
+ valid: [
161
+ {
162
+ name: 'a wrapped non-positive step',
163
+ code: dedent`
164
+ declare const n: number;
165
+ for (let i = 0; i < n; i += 0 as number) {
166
+ console.log(i);
167
+ }
168
+ `,
169
+ },
170
+ ],
171
+ invalid: [
172
+ {
173
+ name: 'the loop variable or the step wrapped',
174
+ code: dedent`
175
+ import { range } from 'ts-data-forge';
176
+ declare const n: number;
177
+ for (let i = 0; (i satisfies number) < n; ++i) {
178
+ console.log(i);
179
+ }
180
+ for (let j = 0; j! < n; j += 2 as number) {
181
+ console.log(j);
182
+ }
183
+ `,
184
+ output: dedent`
185
+ import { range } from 'ts-data-forge';
186
+ declare const n: number;
187
+ for (const i of range(0, n)) {
188
+ console.log(i);
189
+ }
190
+ for (const j of range(0, n, 2)) {
191
+ console.log(j);
192
+ }
193
+ `,
194
+ errors: [
195
+ { messageId: 'useRangeForLoop' },
196
+ { messageId: 'useRangeForLoop' },
197
+ ],
198
+ },
199
+ ],
200
+ });
201
+ }, 20000);
202
+
203
+ describe('prefer-range-for-loop with parenthesized operands', () => {
204
+ tester.run('prefer-range-for-loop', preferRangeForLoop, {
205
+ valid: [],
206
+ invalid: [
207
+ {
208
+ name: 'sequence bounds and step keep their parentheses',
209
+ code: dedent`
210
+ import { range } from 'ts-data-forge';
211
+ declare const n: number;
212
+ declare const step: number;
213
+ declare const log: () => void;
214
+ for (let i = (log(), 0); i < (log(), n); i += (log(), step)) {
215
+ console.log(i);
216
+ }
217
+ `,
218
+ output: dedent`
219
+ import { range } from 'ts-data-forge';
220
+ declare const n: number;
221
+ declare const step: number;
222
+ declare const log: () => void;
223
+ for (const i of range((log(), 0), (log(), n), (log(), step))) {
224
+ console.log(i);
225
+ }
226
+ `,
227
+ errors: [{ messageId: 'useRangeForLoop' }],
228
+ },
229
+ ],
230
+ });
231
+ }, 20000);
@@ -1,4 +1,4 @@
1
- import { type ESLintPlugin } from '../types.mjs';
1
+ import type { ESLintPlugin } from '../types.mjs';
2
2
  import { noSideEffectImport } from './no-side-effect-import.mjs';
3
3
  import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
4
4
  import { preferArrIsArray } from './prefer-arr-is-array.mjs';
package/src/types.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- import { type DeepReadonly } from 'ts-type-forge';
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ import type { DeepReadonly } from 'ts-type-forge';
3
3
 
4
4
  export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
5
5