eslint-plugin-ts-data-forge 0.8.6 → 0.10.0

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 (70) hide show
  1. package/README.md +21 -19
  2. package/dist/plugin.d.mts +4 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +2 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +31 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -2
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +51 -4
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  22. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  23. package/dist/rules/prefer-arr-generate.mjs +588 -0
  24. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  25. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-uniq.mjs +2 -149
  27. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  28. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  29. package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
  30. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  31. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-non-null-object.mjs +110 -40
  33. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  34. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  35. package/dist/rules/prefer-is-record-and-has-key.mjs +102 -45
  36. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  37. package/dist/rules/prefer-pipe-map-nullable.d.mts +24 -0
  38. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -0
  39. package/dist/rules/prefer-pipe-map-nullable.mjs +266 -0
  40. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -0
  41. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  42. package/dist/rules/prefer-range-for-loop.mjs +97 -16
  43. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  44. package/dist/rules/rules.d.mts +2 -0
  45. package/dist/rules/rules.d.mts.map +1 -1
  46. package/dist/rules/rules.mjs +4 -0
  47. package/dist/rules/rules.mjs.map +1 -1
  48. package/package.json +11 -11
  49. package/src/plugin.mts +2 -0
  50. package/src/rule-types.mts +33 -1
  51. package/src/rules/ast-utils.mts +72 -5
  52. package/src/rules/constants.mts +12 -0
  53. package/src/rules/mutable-result.mts +204 -0
  54. package/src/rules/prefer-arr-generate.mts +918 -0
  55. package/src/rules/prefer-arr-generate.test.mts +967 -0
  56. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  57. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  58. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  59. package/src/rules/prefer-arr-uniq.mts +2 -207
  60. package/src/rules/prefer-canonical-array-slicing.mts +57 -40
  61. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  62. package/src/rules/prefer-is-non-null-object.mts +160 -57
  63. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  64. package/src/rules/prefer-is-record-and-has-key.mts +148 -67
  65. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  66. package/src/rules/prefer-pipe-map-nullable.mts +391 -0
  67. package/src/rules/prefer-pipe-map-nullable.test.mts +470 -0
  68. package/src/rules/prefer-range-for-loop.mts +142 -21
  69. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  70. package/src/rules/rules.mts +4 -0
@@ -0,0 +1,470 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferPipeMapNullable } from './prefer-pipe-map-nullable.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ projectService: {
13
+ allowDefaultProject: ['*.ts*'],
14
+ },
15
+ tsconfigRootDir: `${import.meta.dirname}/../..`,
16
+ },
17
+ },
18
+ });
19
+
20
+ /** The same rule with no type information. */
21
+ const untypedTester = new RuleTester({
22
+ languageOptions: {
23
+ parser,
24
+ parserOptions: {
25
+ ecmaVersion: 2020,
26
+ sourceType: 'module',
27
+ },
28
+ },
29
+ });
30
+
31
+ describe('prefer-pipe-map-nullable', () => {
32
+ tester.run('prefer-pipe-map-nullable', preferPipeMapNullable, {
33
+ valid: [
34
+ {
35
+ name: 'ignores pipe(x).mapNullable(fn)',
36
+ code: dedent`
37
+ import { pipe } from 'ts-data-forge';
38
+ declare const x: number | undefined;
39
+ const y = pipe(x).mapNullable((v) => v + 1).value;
40
+ `,
41
+ },
42
+ {
43
+ name: 'ignores map with another function',
44
+ code: dedent`
45
+ import { pipe } from 'ts-data-forge';
46
+ declare const x: number | undefined;
47
+ declare const f: (v: number | undefined) => number;
48
+ const y = pipe(x).map(f).value;
49
+ `,
50
+ },
51
+ {
52
+ name: 'ignores the two-argument mapNullable(value, fn)',
53
+ code: dedent`
54
+ import { mapNullable, pipe } from 'ts-data-forge';
55
+ declare const x: number | undefined;
56
+ declare const g: (v: number | undefined) => (w: number) => number;
57
+ const y = pipe(x).map((v) => mapNullable(v, (w) => w + 1)).value;
58
+ const z = pipe(x).map(g(mapNullable(x, (w) => w + 1))).value;
59
+ `,
60
+ },
61
+ {
62
+ name: 'ignores mapNullable with explicit type arguments',
63
+ code: dedent`
64
+ import { mapNullable, pipe } from 'ts-data-forge';
65
+ declare const x: number | undefined;
66
+ const y = pipe(x).map(mapNullable<number, number>((v) => v + 1)).value;
67
+ `,
68
+ },
69
+ {
70
+ name: 'ignores map with explicit type arguments',
71
+ code: dedent`
72
+ import { mapNullable, pipe } from 'ts-data-forge';
73
+ declare const x: number | undefined;
74
+ const y = pipe(x).map<number | undefined>(mapNullable((v: number) => v + 1)).value;
75
+ `,
76
+ },
77
+ {
78
+ name: 'ignores a mapNullable not imported from ts-data-forge',
79
+ code: dedent`
80
+ import { pipe } from 'ts-data-forge';
81
+ const mapNullable =
82
+ <A, B>(fn: (a: A) => B) =>
83
+ (a: A | null | undefined): B | undefined =>
84
+ a == null ? undefined : fn(a);
85
+ declare const x: number | undefined;
86
+ const y = pipe(x).map(mapNullable((v: number) => v + 1)).value;
87
+ `,
88
+ },
89
+ {
90
+ name: 'ignores a mapNullable that shadows the import',
91
+ code: dedent`
92
+ import { mapNullable as _m, pipe } from 'ts-data-forge';
93
+ import { mapNullable } from 'ts-data-forge';
94
+ declare const x: number | undefined;
95
+ const run = (mapNullable: (fn: (v: number) => number) => (v: number | undefined) => number) =>
96
+ pipe(x).map(mapNullable((v) => v + 1)).value;
97
+ `,
98
+ },
99
+ {
100
+ name: 'ignores a map on something that is not a pipe',
101
+ code: dedent`
102
+ import { mapNullable } from 'ts-data-forge';
103
+ declare const xs: readonly (number | undefined)[];
104
+ const ys = xs.map(mapNullable((v) => v + 1));
105
+ `,
106
+ },
107
+ {
108
+ name: 'ignores a pipe not imported from ts-data-forge',
109
+ code: dedent`
110
+ import { mapNullable } from 'ts-data-forge';
111
+ import { pipe } from 'some-other-pipe';
112
+ declare const x: number | undefined;
113
+ const y = pipe(x).map(mapNullable((v: number) => v + 1));
114
+ `,
115
+ },
116
+ {
117
+ name: 'ignores a pipe held in a variable',
118
+ code: dedent`
119
+ import { mapNullable, pipe } from 'ts-data-forge';
120
+ declare const x: number | undefined;
121
+ const p = pipe(x);
122
+ const y = p.map(mapNullable((v) => v + 1)).value;
123
+ `,
124
+ },
125
+ {
126
+ name: 'ignores optional calls',
127
+ code: dedent`
128
+ import { mapNullable, pipe } from 'ts-data-forge';
129
+ declare const x: number | undefined;
130
+ const y = pipe(x).map?.(mapNullable((v) => v + 1));
131
+ `,
132
+ },
133
+ {
134
+ name: 'ignores a spread argument',
135
+ code: dedent`
136
+ import { mapNullable, pipe } from 'ts-data-forge';
137
+ declare const x: number | undefined;
138
+ declare const args: readonly [(v: number) => number];
139
+ const y = pipe(x).map(mapNullable(...args));
140
+ `,
141
+ },
142
+ ],
143
+ invalid: [
144
+ {
145
+ name: 'fixes an inline function whose result type is not a literal',
146
+ code: dedent`
147
+ import { mapNullable, pipe } from 'ts-data-forge';
148
+ declare const x: number | undefined;
149
+ const y = pipe(x).map(mapNullable((v) => v + 1)).value;
150
+ `,
151
+ output: dedent`
152
+ import { mapNullable, pipe } from 'ts-data-forge';
153
+ declare const x: number | undefined;
154
+ const y = pipe(x).mapNullable((v) => v + 1).value;
155
+ `,
156
+ errors: [{ messageId: 'useMapNullable' }],
157
+ },
158
+ {
159
+ name: 'fixes a function reference',
160
+ code: dedent`
161
+ import { mapNullable, pipe } from 'ts-data-forge';
162
+ declare const x: string | undefined;
163
+ declare const f: (s: string) => 'a';
164
+ const y = pipe(x).map(mapNullable(f)).value;
165
+ `,
166
+ output: dedent`
167
+ import { mapNullable, pipe } from 'ts-data-forge';
168
+ declare const x: string | undefined;
169
+ declare const f: (s: string) => 'a';
170
+ const y = pipe(x).mapNullable(f).value;
171
+ `,
172
+ errors: [{ messageId: 'useMapNullable' }],
173
+ },
174
+ {
175
+ name: 'fixes an inline function with a return type annotation',
176
+ code: dedent`
177
+ import { mapNullable, pipe } from 'ts-data-forge';
178
+ declare const x: number | undefined;
179
+ const y = pipe(x).map(mapNullable((v): 'a' => 'a')).value;
180
+ `,
181
+ output: dedent`
182
+ import { mapNullable, pipe } from 'ts-data-forge';
183
+ declare const x: number | undefined;
184
+ const y = pipe(x).mapNullable((v): 'a' => 'a').value;
185
+ `,
186
+ errors: [{ messageId: 'useMapNullable' }],
187
+ },
188
+ {
189
+ name: 'fixes an inline function returning an object of non-literals',
190
+ code: dedent`
191
+ import { mapNullable, pipe } from 'ts-data-forge';
192
+ declare const x: number | undefined;
193
+ const y = pipe(x).map(mapNullable((v) => ({ v, ok: v > 0 }))).value;
194
+ `,
195
+ output: dedent`
196
+ import { mapNullable, pipe } from 'ts-data-forge';
197
+ declare const x: number | undefined;
198
+ const y = pipe(x).mapNullable((v) => ({ v, ok: v > 0 })).value;
199
+ `,
200
+ errors: [{ messageId: 'useMapNullable' }],
201
+ },
202
+ {
203
+ name: 'fixes every link of a chain',
204
+ code: dedent`
205
+ import { mapNullable, pipe } from 'ts-data-forge';
206
+ declare const x: string | undefined;
207
+ const y = pipe(x)
208
+ .map(mapNullable((s) => s.length))
209
+ .map((n) => n)
210
+ .map(mapNullable((n) => n * 2)).value;
211
+ `,
212
+ output: dedent`
213
+ import { mapNullable, pipe } from 'ts-data-forge';
214
+ declare const x: string | undefined;
215
+ const y = pipe(x)
216
+ .mapNullable((s) => s.length)
217
+ .map((n) => n)
218
+ .mapNullable((n) => n * 2).value;
219
+ `,
220
+ errors: [
221
+ { messageId: 'useMapNullable' },
222
+ { messageId: 'useMapNullable' },
223
+ ],
224
+ },
225
+ {
226
+ name: 'fixes after mapOptional and mapNullable links',
227
+ code: dedent`
228
+ import { mapNullable, Optional, pipe } from 'ts-data-forge';
229
+ declare const o: Optional<number>;
230
+ const y = pipe(o)
231
+ .mapOptional((n) => n + 1)
232
+ .map((p) => Optional.unwrapOr(p, undefined))
233
+ .mapNullable((n) => n)
234
+ .map(mapNullable((n) => n * 2)).value;
235
+ `,
236
+ output: dedent`
237
+ import { mapNullable, Optional, pipe } from 'ts-data-forge';
238
+ declare const o: Optional<number>;
239
+ const y = pipe(o)
240
+ .mapOptional((n) => n + 1)
241
+ .map((p) => Optional.unwrapOr(p, undefined))
242
+ .mapNullable((n) => n)
243
+ .mapNullable((n) => n * 2).value;
244
+ `,
245
+ errors: [{ messageId: 'useMapNullable' }],
246
+ },
247
+ {
248
+ name: 'fixes aliased and namespace imports',
249
+ code: dedent`
250
+ import * as tdf from 'ts-data-forge';
251
+ import { mapNullable as mapN } from 'ts-data-forge';
252
+ declare const x: number | undefined;
253
+ const y = tdf.pipe(x).map(tdf.mapNullable((v) => v + 1)).value;
254
+ const z = tdf.pipe(x).map(mapN((v) => v + 1)).value;
255
+ `,
256
+ output: dedent`
257
+ import * as tdf from 'ts-data-forge';
258
+ import { mapNullable as mapN } from 'ts-data-forge';
259
+ declare const x: number | undefined;
260
+ const y = tdf.pipe(x).mapNullable((v) => v + 1).value;
261
+ const z = tdf.pipe(x).mapNullable((v) => v + 1).value;
262
+ `,
263
+ errors: [
264
+ { messageId: 'useMapNullable' },
265
+ { messageId: 'useMapNullable' },
266
+ ],
267
+ },
268
+ {
269
+ name: 'fixes a pipe imported from ts-std-forge',
270
+ code: dedent`
271
+ import { mapNullable } from 'ts-data-forge';
272
+ import { pipe } from 'ts-std-forge';
273
+ declare const x: number | undefined;
274
+ const y = pipe(x).map(mapNullable((v) => v + 1)).value;
275
+ `,
276
+ output: dedent`
277
+ import { mapNullable } from 'ts-data-forge';
278
+ import { pipe } from 'ts-std-forge';
279
+ declare const x: number | undefined;
280
+ const y = pipe(x).mapNullable((v) => v + 1).value;
281
+ `,
282
+ errors: [{ messageId: 'useMapNullable' }],
283
+ },
284
+ {
285
+ name: 'keeps comments around the function and a sequence argument',
286
+ code: dedent`
287
+ import { mapNullable, pipe } from 'ts-data-forge';
288
+ declare const x: number | undefined;
289
+ declare const f: (v: number) => number;
290
+ const y = pipe(x).map(/* a */ mapNullable((0, f)) /* b */).value;
291
+ `,
292
+ output: dedent`
293
+ import { mapNullable, pipe } from 'ts-data-forge';
294
+ declare const x: number | undefined;
295
+ declare const f: (v: number) => number;
296
+ const y = pipe(x).mapNullable(/* a */ (0, f) /* b */).value;
297
+ `,
298
+ errors: [{ messageId: 'useMapNullable' }],
299
+ },
300
+ {
301
+ name: 'suggests when the inferred result is a literal',
302
+ code: dedent`
303
+ import { mapNullable, pipe } from 'ts-data-forge';
304
+ declare const x: number | undefined;
305
+ const y = pipe(x).map(mapNullable((v) => 'a')).value;
306
+ `,
307
+ errors: [
308
+ {
309
+ messageId: 'useMapNullable',
310
+ suggestions: [
311
+ {
312
+ messageId: 'suggestMapNullable',
313
+ output: dedent`
314
+ import { mapNullable, pipe } from 'ts-data-forge';
315
+ declare const x: number | undefined;
316
+ const y = pipe(x).mapNullable((v) => 'a').value;
317
+ `,
318
+ },
319
+ ],
320
+ },
321
+ ],
322
+ },
323
+ {
324
+ name: 'suggests when the inferred result is a tuple',
325
+ code: dedent`
326
+ import { mapNullable, pipe } from 'ts-data-forge';
327
+ declare const x: number | undefined;
328
+ const y = pipe(x).map(mapNullable((v) => [v, 1])).value;
329
+ `,
330
+ errors: [
331
+ {
332
+ messageId: 'useMapNullable',
333
+ suggestions: [
334
+ {
335
+ messageId: 'suggestMapNullable',
336
+ output: dedent`
337
+ import { mapNullable, pipe } from 'ts-data-forge';
338
+ declare const x: number | undefined;
339
+ const y = pipe(x).mapNullable((v) => [v, 1]).value;
340
+ `,
341
+ },
342
+ ],
343
+ },
344
+ ],
345
+ },
346
+ {
347
+ name: 'suggests when a literal is nested in an object',
348
+ code: dedent`
349
+ import { mapNullable, pipe } from 'ts-data-forge';
350
+ declare const x: number | undefined;
351
+ const y = pipe(x).map(mapNullable((v) => ({ kind: 'n', v }))).value;
352
+ `,
353
+ errors: [
354
+ {
355
+ messageId: 'useMapNullable',
356
+ suggestions: [
357
+ {
358
+ messageId: 'suggestMapNullable',
359
+ output: dedent`
360
+ import { mapNullable, pipe } from 'ts-data-forge';
361
+ declare const x: number | undefined;
362
+ const y = pipe(x).mapNullable((v) => ({ kind: 'n', v })).value;
363
+ `,
364
+ },
365
+ ],
366
+ },
367
+ ],
368
+ },
369
+ {
370
+ name: 'suggests when a comment inside mapNullable(…) would be dropped',
371
+ code: dedent`
372
+ import { mapNullable, pipe } from 'ts-data-forge';
373
+ declare const x: number | undefined;
374
+ declare const f: (v: number) => number;
375
+ const y = pipe(x).map(mapNullable(/* keep */ f)).value;
376
+ `,
377
+ errors: [
378
+ {
379
+ messageId: 'useMapNullable',
380
+ suggestions: [
381
+ {
382
+ messageId: 'suggestMapNullable',
383
+ output: dedent`
384
+ import { mapNullable, pipe } from 'ts-data-forge';
385
+ declare const x: number | undefined;
386
+ declare const f: (v: number) => number;
387
+ const y = pipe(x).mapNullable(f).value;
388
+ `,
389
+ },
390
+ ],
391
+ },
392
+ ],
393
+ },
394
+ {
395
+ name: 'suggests when a type wrapper around mapNullable would be dropped',
396
+ code: dedent`
397
+ import { mapNullable, pipe } from 'ts-data-forge';
398
+ declare const x: number | undefined;
399
+ declare const f: (v: number) => number;
400
+ const y = pipe(x).map((mapNullable as typeof mapNullable)(f)).value;
401
+ `,
402
+ errors: [
403
+ {
404
+ messageId: 'useMapNullable',
405
+ suggestions: [
406
+ {
407
+ messageId: 'suggestMapNullable',
408
+ output: dedent`
409
+ import { mapNullable, pipe } from 'ts-data-forge';
410
+ declare const x: number | undefined;
411
+ declare const f: (v: number) => number;
412
+ const y = pipe(x).mapNullable(f).value;
413
+ `,
414
+ },
415
+ ],
416
+ },
417
+ ],
418
+ },
419
+ ],
420
+ });
421
+
422
+ untypedTester.run(
423
+ 'prefer-pipe-map-nullable (no type information)',
424
+ preferPipeMapNullable,
425
+ {
426
+ valid: [],
427
+ invalid: [
428
+ {
429
+ name: 'fixes a function reference',
430
+ code: dedent`
431
+ import { mapNullable, pipe } from 'ts-data-forge';
432
+ declare const x: number | undefined;
433
+ declare const f: (v: number) => number;
434
+ const y = pipe(x).map(mapNullable(f)).value;
435
+ `,
436
+ output: dedent`
437
+ import { mapNullable, pipe } from 'ts-data-forge';
438
+ declare const x: number | undefined;
439
+ declare const f: (v: number) => number;
440
+ const y = pipe(x).mapNullable(f).value;
441
+ `,
442
+ errors: [{ messageId: 'useMapNullable' }],
443
+ },
444
+ {
445
+ name: 'suggests for an inline function it cannot type',
446
+ code: dedent`
447
+ import { mapNullable, pipe } from 'ts-data-forge';
448
+ declare const x: number | undefined;
449
+ const y = pipe(x).map(mapNullable((v) => v + 1)).value;
450
+ `,
451
+ errors: [
452
+ {
453
+ messageId: 'useMapNullable',
454
+ suggestions: [
455
+ {
456
+ messageId: 'suggestMapNullable',
457
+ output: dedent`
458
+ import { mapNullable, pipe } from 'ts-data-forge';
459
+ declare const x: number | undefined;
460
+ const y = pipe(x).mapNullable((v) => v + 1).value;
461
+ `,
462
+ },
463
+ ],
464
+ },
465
+ ],
466
+ },
467
+ ],
468
+ },
469
+ );
470
+ });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AST_NODE_TYPES,
3
+ ASTUtils,
3
4
  type TSESLint,
4
5
  type TSESTree,
5
6
  } from '@typescript-eslint/utils';
@@ -42,12 +43,47 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
42
43
  TS_DATA_FORGE_MODULE,
43
44
  );
44
45
 
46
+ /**
47
+ * Whether `node` is the same value on every iteration: a literal, or a
48
+ * variable that is never reassigned anywhere (not only in the loop, since
49
+ * a function the loop calls could reassign it). The loop reads its end and
50
+ * its step on every iteration, and `range` reads them once.
51
+ */
52
+ const isStableBound = (
53
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
54
+ wrapped: TSESTree.Node,
55
+ ): boolean => {
56
+ const node = skipTypeWrappers(wrapped);
57
+
58
+ if (node.type === AST_NODE_TYPES.Literal) {
59
+ return true;
60
+ }
61
+
62
+ if (node.type !== AST_NODE_TYPES.Identifier) {
63
+ return false;
64
+ }
65
+
66
+ const variable = ASTUtils.findVariable(
67
+ sourceCode.getScope(node),
68
+ node.name,
69
+ );
70
+
71
+ return (
72
+ variable !== null &&
73
+ variable.defs.length > 0 &&
74
+ variable.references.every(
75
+ (reference) => !reference.isWrite() || reference.init === true,
76
+ )
77
+ );
78
+ };
79
+
45
80
  const mut_nodesToFix: {
46
81
  node: TSESTree.ForStatement;
47
82
  varName: string;
48
83
  begin: string;
49
84
  end: string;
50
85
  step?: string;
86
+ fixable: boolean;
51
87
  }[] = [];
52
88
 
53
89
  return {
@@ -74,6 +110,11 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
74
110
 
75
111
  const beginExpr = declaration.init;
76
112
 
113
+ // `range` takes safe integers only.
114
+ if (isNonIntegerNumberLiteral(beginExpr)) {
115
+ return;
116
+ }
117
+
77
118
  // Check test: i < end. The loop variable and the step are read
78
119
  // through type wrappers: `(i satisfies number) < n` tests `i`, and
79
120
  // `i += 0 as number` still steps by 0.
@@ -126,19 +167,35 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
126
167
  const stepNum = stepValue.value;
127
168
 
128
169
  // Only support positive integer steps
129
- if (typeof stepNum === 'number' && stepNum > 0) {
170
+ if (
171
+ typeof stepNum === 'number' &&
172
+ Number.isSafeInteger(stepNum) &&
173
+ stepNum > 0
174
+ ) {
130
175
  mut_step = stepNum === 1 ? undefined : stepNum.toString();
131
176
  } else {
132
177
  return;
133
178
  }
179
+ } else if (isNegatedLiteral(stepValue)) {
180
+ // `i += -1` under `i < end` never gets there, and `range` with a
181
+ // negative step yields nothing: no fix that type-checks would
182
+ // keep that.
183
+ return;
134
184
  } else {
135
- // Variable step like i += step
185
+ // Variable step like i += step. Its type is left to the type
186
+ // checker: a step that is not a non-zero safe integer makes the
187
+ // fixed `range(...)` call fail to type-check.
136
188
  mut_step = getArgumentText(sourceCode, node.update.right);
137
189
  }
138
190
  } else {
139
191
  return;
140
192
  }
141
193
 
194
+ // `const i` would not take an assignment anywhere but the update.
195
+ if (isAssignedOutsideUpdate(sourceCode, declaration, node.update)) {
196
+ return;
197
+ }
198
+
142
199
  const beginText = getArgumentText(sourceCode, beginExpr);
143
200
 
144
201
  const endText = getArgumentText(sourceCode, endExpr);
@@ -149,6 +206,10 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
149
206
  begin: beginText,
150
207
  end: endText,
151
208
  step: mut_step,
209
+ fixable:
210
+ isStableBound(endExpr) &&
211
+ (node.update.type !== AST_NODE_TYPES.AssignmentExpression ||
212
+ isStableBound(node.update.right)),
152
213
  });
153
214
  },
154
215
  'Program:exit': () => {
@@ -156,10 +217,11 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
156
217
 
157
218
  const hasRangeImport = namedImports.includes('range');
158
219
 
159
- for (const [
160
- index,
161
- { node, varName, begin, end, step },
162
- ] of mut_nodesToFix.entries()) {
220
+ const importCarrier = mut_nodesToFix.find(({ fixable }) => fixable);
221
+
222
+ for (const entry of mut_nodesToFix) {
223
+ const { node, varName, begin, end, step } = entry;
224
+
163
225
  context.report({
164
226
  node: node.init ?? node,
165
227
  messageId: 'useRangeForLoop',
@@ -168,25 +230,33 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
168
230
  begin,
169
231
  end,
170
232
  },
171
- fix: (fixer) => {
172
- const bodyText = sourceCode.getText(node.body);
233
+ fix: entry.fixable
234
+ ? (fixer) => {
235
+ const bodyText = sourceCode.getText(node.body);
173
236
 
174
- const rangeArgs =
175
- step === undefined
176
- ? `${begin}, ${end}`
177
- : `${begin}, ${end}, ${step}`;
237
+ const rangeArgs =
238
+ step === undefined
239
+ ? `${begin}, ${end}`
240
+ : `${begin}, ${end}, ${step}`;
178
241
 
179
- const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
242
+ const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
180
243
 
181
- const importFixes =
182
- index === 0 && !hasRangeImport
183
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
184
- 'range',
185
- ])
186
- : [];
244
+ // The import fix lives on the first fixable report:
245
+ // `insertTextBefore(program, …)` produces overlapping ranges
246
+ // otherwise.
247
+ const importFixes =
248
+ entry === importCarrier && !hasRangeImport
249
+ ? buildNamedImportFix(
250
+ fixer,
251
+ program,
252
+ TS_DATA_FORGE_MODULE,
253
+ ['range'],
254
+ )
255
+ : [];
187
256
 
188
- return [...importFixes, fixer.replaceText(node, replacement)];
189
- },
257
+ return [...importFixes, fixer.replaceText(node, replacement)];
258
+ }
259
+ : undefined,
190
260
  });
191
261
  }
192
262
  },
@@ -194,3 +264,54 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
194
264
  },
195
265
  defaultOptions: [],
196
266
  } as const;
267
+
268
+ const isNegatedLiteral = (
269
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
270
+ node: TSESTree.Node,
271
+ ): boolean =>
272
+ node.type === AST_NODE_TYPES.UnaryExpression &&
273
+ node.operator === '-' &&
274
+ skipTypeWrappers(node.argument).type === AST_NODE_TYPES.Literal;
275
+
276
+ const isNonIntegerNumberLiteral = (
277
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
278
+ wrapped: TSESTree.Node,
279
+ ): boolean => {
280
+ const node = skipTypeWrappers(wrapped);
281
+
282
+ return (
283
+ node.type === AST_NODE_TYPES.Literal &&
284
+ typeof node.value === 'number' &&
285
+ !Number.isSafeInteger(node.value)
286
+ );
287
+ };
288
+
289
+ /**
290
+ * Whether the loop variable declared by `declaration` is written anywhere but
291
+ * its initializer and the loop's `update`.
292
+ */
293
+ const isAssignedOutsideUpdate = (
294
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
295
+ sourceCode: TSESLint.SourceCode,
296
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
297
+ declaration: TSESTree.VariableDeclarator,
298
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
299
+ update: TSESTree.Expression,
300
+ ): boolean =>
301
+ sourceCode
302
+ .getDeclaredVariables(declaration)
303
+ .flatMap((variable) => variable.references)
304
+ .some(
305
+ (reference) =>
306
+ reference.isWrite() &&
307
+ reference.init !== true &&
308
+ !isWithin(reference.identifier, update),
309
+ );
310
+
311
+ const isWithin = (
312
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
313
+ node: TSESTree.Node,
314
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
315
+ container: TSESTree.Node,
316
+ ): boolean =>
317
+ container.range[0] <= node.range[0] && node.range[1] <= container.range[1];