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,967 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferArrGenerate } from './prefer-arr-generate.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
+ describe('prefer-arr-generate', () => {
21
+ tester.run('prefer-arr-generate', preferArrGenerate, {
22
+ valid: [
23
+ {
24
+ name: 'ignores Arr.generate',
25
+ code: dedent`
26
+ import { Arr } from 'ts-data-forge';
27
+ declare const xs: readonly number[];
28
+ const ys = Arr.generate(function* () {
29
+ for (const x of xs) {
30
+ if (x > 0) {
31
+ yield x;
32
+ }
33
+ }
34
+ });
35
+ `,
36
+ },
37
+ {
38
+ name: 'ignores a callback that is not a conditional',
39
+ code: dedent`
40
+ declare const xs: readonly number[];
41
+ const ys = xs.flatMap((x) => [x, x]);
42
+ `,
43
+ },
44
+ {
45
+ name: 'ignores a conditional whose branches are both non-empty',
46
+ code: dedent`
47
+ declare const xs: readonly number[];
48
+ const ys = xs.flatMap((x) => (x > 0 ? [x] : [-x]));
49
+ `,
50
+ },
51
+ {
52
+ name: 'ignores a conditional whose branches are both empty',
53
+ code: dedent`
54
+ declare const xs: readonly number[];
55
+ const ys = xs.flatMap((x) => (x > 0 ? [] : []));
56
+ `,
57
+ },
58
+ {
59
+ name: 'ignores a spread in the kept branch',
60
+ code: dedent`
61
+ declare const xs: readonly (readonly number[])[];
62
+ const ys = xs.flatMap((x) => (x.length > 0 ? [...x] : []));
63
+ `,
64
+ },
65
+ {
66
+ name: 'ignores a callback that reads the index or the array',
67
+ code: dedent`
68
+ declare const xs: readonly number[];
69
+ const ys = xs.flatMap((x, i) => (i > 0 ? [x] : []));
70
+ const zs = xs.flatMap((x, _i, all) => (all.length > 1 ? [x] : []));
71
+ `,
72
+ },
73
+ {
74
+ name: 'ignores a callback with a default or a rest parameter',
75
+ code: dedent`
76
+ declare const xs: readonly (number | undefined)[];
77
+ const ys = xs.flatMap((x = 0) => (x > 0 ? [x] : []));
78
+ const zs = xs.flatMap((...args) => (args.length > 0 ? [args] : []));
79
+ `,
80
+ },
81
+ {
82
+ name: 'ignores an optional parameter',
83
+ code: dedent`
84
+ declare const xs: readonly number[];
85
+ const ys = xs.flatMap((x?: number) => (x !== undefined ? [x] : []));
86
+ `,
87
+ },
88
+ {
89
+ name: 'ignores a thisArg',
90
+ code: dedent`
91
+ declare const xs: readonly number[];
92
+ declare const self: unknown;
93
+ const ys = xs.flatMap((x) => (x > 0 ? [x] : []), self);
94
+ `,
95
+ },
96
+ {
97
+ name: 'ignores a function expression and an async arrow',
98
+ code: dedent`
99
+ declare const xs: readonly number[];
100
+ const ys = xs.flatMap(function (x) {
101
+ return x > 0 ? [x] : [];
102
+ });
103
+ const zs = xs.flatMap(async (x) => (x > 0 ? [x] : []));
104
+ `,
105
+ },
106
+ {
107
+ name: 'ignores a receiver that is not an array',
108
+ code: dedent`
109
+ declare const o: Readonly<{
110
+ flatMap: (f: (x: number) => readonly number[]) => readonly number[];
111
+ }>;
112
+ const ys = o.flatMap((x) => (x > 0 ? [x] : []));
113
+ `,
114
+ },
115
+ {
116
+ name: 'ignores an optional call',
117
+ code: dedent`
118
+ declare const xs: readonly number[] | undefined;
119
+ const ys = xs?.flatMap((x) => (x > 0 ? [x] : []));
120
+ `,
121
+ },
122
+ {
123
+ name: 'ignores `this` and `arguments`, which the generator function would rebind',
124
+ code: dedent`
125
+ class C {
126
+ readonly xs: readonly number[] = [];
127
+ readonly k = 1;
128
+ f(): readonly number[] {
129
+ return this.xs.flatMap((x) => (x > this.k ? [x] : []));
130
+ }
131
+ }
132
+ function g(xs: readonly number[]): readonly unknown[] {
133
+ return xs.flatMap((x) => (x > 0 ? [arguments.length] : []));
134
+ }
135
+ `,
136
+ },
137
+ {
138
+ name: 'ignores a receiver that names the parameter, which the loop would shadow',
139
+ code: dedent`
140
+ type Node = Readonly<{ id: number; children: readonly Node[] }>;
141
+ declare const node: Node;
142
+ const ys = node.children.flatMap((node) => (node.id > 0 ? [node] : []));
143
+ `,
144
+ },
145
+ {
146
+ name: 'ignores a block body with another return',
147
+ code: dedent`
148
+ declare const xs: readonly number[];
149
+ const ys = xs.flatMap((x) => {
150
+ if (x > 10) {
151
+ return [x, x];
152
+ }
153
+ return x > 0 ? [x] : [];
154
+ });
155
+ `,
156
+ },
157
+ {
158
+ name: 'ignores a block body that does not end in the conditional',
159
+ code: dedent`
160
+ declare const xs: readonly number[];
161
+ const ys = xs.flatMap((x) => {
162
+ const y = x > 0 ? [x] : [];
163
+ return y;
164
+ });
165
+ `,
166
+ },
167
+ {
168
+ name: 'ignores a callback wrapped in a type assertion, which may be what types its parameter',
169
+ code: dedent`
170
+ declare const xs: readonly number[];
171
+ const ys = xs.flatMap(((x) => (x > 0 ? [x] : [])) as (
172
+ x: number,
173
+ ) => readonly number[]);
174
+ `,
175
+ },
176
+ ],
177
+ invalid: [
178
+ {
179
+ name: 'replaces a filter-and-map flatMap',
180
+ code: dedent`
181
+ declare const xs: readonly number[];
182
+ const ys = xs.flatMap((x) => (x > 0 ? [x * 2] : []));
183
+ `,
184
+ output: dedent`
185
+ import { Arr } from 'ts-data-forge';
186
+ declare const xs: readonly number[];
187
+ const ys = Arr.generate(function* () {
188
+ for (const x of xs) {
189
+ if (x > 0) {
190
+ yield x * 2;
191
+ }
192
+ }
193
+ });
194
+ `,
195
+ errors: [{ messageId: 'useArrGenerate' }],
196
+ },
197
+ {
198
+ name: 'negates the condition when the empty array comes first',
199
+ code: dedent`
200
+ declare const xs: readonly (number | undefined)[];
201
+ declare const skip: boolean;
202
+ const ys = xs.flatMap((x) => (x === undefined ? [] : [x]));
203
+ const zs = xs.flatMap((x) => (skip ? [] : [x]));
204
+ `,
205
+ output: dedent`
206
+ import { Arr } from 'ts-data-forge';
207
+ declare const xs: readonly (number | undefined)[];
208
+ declare const skip: boolean;
209
+ const ys = Arr.generate(function* () {
210
+ for (const x of xs) {
211
+ if (x !== undefined) {
212
+ yield x;
213
+ }
214
+ }
215
+ });
216
+ const zs = Arr.generate(function* () {
217
+ for (const x of xs) {
218
+ if (!skip) {
219
+ yield x;
220
+ }
221
+ }
222
+ });
223
+ `,
224
+ errors: [
225
+ { messageId: 'useArrGenerate' },
226
+ { messageId: 'useArrGenerate' },
227
+ ],
228
+ },
229
+ {
230
+ name: 'negates the short way: an inverted comparison, De Morgan, and a dropped double negation, but a comparison that may be NaN only with !',
231
+ code: dedent`
232
+ declare const xs: readonly (string | undefined)[];
233
+ declare const ok: (x: string | undefined) => boolean;
234
+ declare const n: number;
235
+ const a = xs.flatMap((x) => (x === undefined || x === '' ? [] : [x]));
236
+ const b = xs.flatMap((x) => (ok(x) ? [] : [x]));
237
+ const c = xs.flatMap((x) => (n < 0 ? [] : [x]));
238
+ const d = xs.flatMap((x) => (!ok(x) ? [] : [x]));
239
+ `,
240
+ output: dedent`
241
+ import { Arr } from 'ts-data-forge';
242
+ declare const xs: readonly (string | undefined)[];
243
+ declare const ok: (x: string | undefined) => boolean;
244
+ declare const n: number;
245
+ const a = Arr.generate(function* () {
246
+ for (const x of xs) {
247
+ if (x !== undefined && x !== '') {
248
+ yield x;
249
+ }
250
+ }
251
+ });
252
+ const b = Arr.generate(function* () {
253
+ for (const x of xs) {
254
+ if (!ok(x)) {
255
+ yield x;
256
+ }
257
+ }
258
+ });
259
+ const c = Arr.generate(function* () {
260
+ for (const x of xs) {
261
+ if (!(n < 0)) {
262
+ yield x;
263
+ }
264
+ }
265
+ });
266
+ const d = Arr.generate(function* () {
267
+ for (const x of xs) {
268
+ if (ok(x)) {
269
+ yield x;
270
+ }
271
+ }
272
+ });
273
+ `,
274
+ errors: [
275
+ { messageId: 'useArrGenerate' },
276
+ { messageId: 'useArrGenerate' },
277
+ { messageId: 'useArrGenerate' },
278
+ { messageId: 'useArrGenerate' },
279
+ ],
280
+ },
281
+ {
282
+ name: 'moves the statements of a block body into the loop',
283
+ code: dedent`
284
+ declare const xs: readonly string[];
285
+ declare const lookup: (key: string) => number | undefined;
286
+ const ys = xs.flatMap((key) => {
287
+ const value = lookup(key);
288
+
289
+ // Only the keys that resolve.
290
+ return value === undefined ? [] : [[key, value] as const];
291
+ });
292
+ `,
293
+ output: dedent`
294
+ import { Arr } from 'ts-data-forge';
295
+ declare const xs: readonly string[];
296
+ declare const lookup: (key: string) => number | undefined;
297
+ const ys = Arr.generate(function* () {
298
+ for (const key of xs) {
299
+ const value = lookup(key);
300
+
301
+ // Only the keys that resolve.
302
+ if (value !== undefined) {
303
+ yield [key, value] as const;
304
+ }
305
+ }
306
+ });
307
+ `,
308
+ errors: [{ messageId: 'useArrGenerate' }],
309
+ },
310
+ {
311
+ name: 'leaves the lines of a multi-line template literal as they are',
312
+ code: [
313
+ 'declare const xs: readonly string[];',
314
+ 'const ys = xs.flatMap((x) =>',
315
+ ' x.length > 0',
316
+ ' ? [`first line',
317
+ ' continued`]',
318
+ ' : [],',
319
+ ');',
320
+ ].join('\n'),
321
+ output: [
322
+ "import { Arr } from 'ts-data-forge';",
323
+ 'declare const xs: readonly string[];',
324
+ 'const ys = Arr.generate(function* () {',
325
+ ' for (const x of xs) {',
326
+ ' if (x.length > 0) {',
327
+ ' yield `first line',
328
+ ' continued`;',
329
+ ' }',
330
+ ' }',
331
+ '});',
332
+ ].join('\n'),
333
+ errors: [{ messageId: 'useArrGenerate' }],
334
+ },
335
+ {
336
+ name: 'moves a comment at the top of a block body into the loop too',
337
+ code: dedent`
338
+ declare const xs: readonly number[];
339
+ const ys = xs.flatMap((x) => {
340
+ // Odd ones only.
341
+ return x % 2 === 1 ? [x] : [];
342
+ });
343
+ `,
344
+ output: dedent`
345
+ import { Arr } from 'ts-data-forge';
346
+ declare const xs: readonly number[];
347
+ const ys = Arr.generate(function* () {
348
+ for (const x of xs) {
349
+ // Odd ones only.
350
+ if (x % 2 === 1) {
351
+ yield x;
352
+ }
353
+ }
354
+ });
355
+ `,
356
+ errors: [{ messageId: 'useArrGenerate' }],
357
+ },
358
+ {
359
+ name: 'yields each element of the kept branch, and keeps a destructuring parameter',
360
+ code: dedent`
361
+ declare const xs: readonly Readonly<{ a: number; b: number }>[];
362
+ const ys = xs.flatMap(({ a, b }) => (a > b ? [a, b] : []));
363
+ `,
364
+ output: dedent`
365
+ import { Arr } from 'ts-data-forge';
366
+ declare const xs: readonly Readonly<{ a: number; b: number }>[];
367
+ const ys = Arr.generate(function* () {
368
+ for (const { a, b } of xs) {
369
+ if (a > b) {
370
+ yield a;
371
+ yield b;
372
+ }
373
+ }
374
+ });
375
+ `,
376
+ errors: [{ messageId: 'useArrGenerate' }],
377
+ },
378
+ {
379
+ name: 'parenthesizes a sequence expression yielded or iterated',
380
+ code: dedent`
381
+ declare const xs: readonly number[];
382
+ declare const f: () => void;
383
+ const ys = (f(), xs).flatMap((x) => (x > 0 ? [(f(), x)] : []));
384
+ `,
385
+ output: dedent`
386
+ import { Arr } from 'ts-data-forge';
387
+ declare const xs: readonly number[];
388
+ declare const f: () => void;
389
+ const ys = Arr.generate(function* () {
390
+ for (const x of (f(), xs)) {
391
+ if (x > 0) {
392
+ yield (f(), x);
393
+ }
394
+ }
395
+ });
396
+ `,
397
+ errors: [{ messageId: 'useArrGenerate' }],
398
+ },
399
+ {
400
+ name: 'keeps an explicit type argument',
401
+ code: dedent`
402
+ declare const xs: readonly number[];
403
+ const ys = xs.flatMap<number | string>((x) => (x > 0 ? [x] : []));
404
+ `,
405
+ output: dedent`
406
+ import { Arr } from 'ts-data-forge';
407
+ declare const xs: readonly number[];
408
+ const ys = Arr.generate<number | string>(function* () {
409
+ for (const x of xs) {
410
+ if (x > 0) {
411
+ yield x;
412
+ }
413
+ }
414
+ });
415
+ `,
416
+ errors: [{ messageId: 'useArrGenerate' }],
417
+ },
418
+ {
419
+ name: 'uses an existing or aliased Arr import',
420
+ code: dedent`
421
+ import { Arr as A } from 'ts-data-forge';
422
+ declare const xs: readonly number[];
423
+ const ys = xs.flatMap((x) => (x > 0 ? [x] : []));
424
+ `,
425
+ output: dedent`
426
+ import { Arr as A } from 'ts-data-forge';
427
+ declare const xs: readonly number[];
428
+ const ys = A.generate(function* () {
429
+ for (const x of xs) {
430
+ if (x > 0) {
431
+ yield x;
432
+ }
433
+ }
434
+ });
435
+ `,
436
+ errors: [{ messageId: 'useArrGenerate' }],
437
+ },
438
+ {
439
+ name: 'indents from the line the call starts on, and accepts a mutable array, a tuple or a type parameter',
440
+ code: dedent`
441
+ declare const ms: number[];
442
+ declare const ts: readonly [number, number];
443
+ export const f = <T extends readonly number[]>(xs: T): void => {
444
+ const a = ms.flatMap((x) => (x > 0 ? [x] : []));
445
+ const b = ts.flatMap((x) => (x > 0 ? [x] : []));
446
+ const c = xs.flatMap((x) => (x > 0 ? [x] : []));
447
+ };
448
+ `,
449
+ output: dedent`
450
+ import { Arr } from 'ts-data-forge';
451
+ declare const ms: number[];
452
+ declare const ts: readonly [number, number];
453
+ export const f = <T extends readonly number[]>(xs: T): void => {
454
+ const a = Arr.generate(function* () {
455
+ for (const x of ms) {
456
+ if (x > 0) {
457
+ yield x;
458
+ }
459
+ }
460
+ });
461
+ const b = Arr.generate(function* () {
462
+ for (const x of ts) {
463
+ if (x > 0) {
464
+ yield x;
465
+ }
466
+ }
467
+ });
468
+ const c = Arr.generate(function* () {
469
+ for (const x of xs) {
470
+ if (x > 0) {
471
+ yield x;
472
+ }
473
+ }
474
+ });
475
+ };
476
+ `,
477
+ errors: [
478
+ { messageId: 'useArrGenerate' },
479
+ { messageId: 'useArrGenerate' },
480
+ { messageId: 'useArrGenerate' },
481
+ ],
482
+ },
483
+ {
484
+ name: 'only suggests when the result is used as a mutable array',
485
+ code: dedent`
486
+ declare const xs: readonly number[];
487
+ const ys = xs.flatMap((x) => (x > 0 ? [x] : []));
488
+ ys.push(1);
489
+ `,
490
+ output: null,
491
+ errors: [
492
+ {
493
+ messageId: 'useArrGenerate',
494
+ suggestions: [
495
+ {
496
+ messageId: 'suggestArrGenerate',
497
+ output: dedent`
498
+ import { Arr } from 'ts-data-forge';
499
+ declare const xs: readonly number[];
500
+ const ys = Arr.generate(function* () {
501
+ for (const x of xs) {
502
+ if (x > 0) {
503
+ yield x;
504
+ }
505
+ }
506
+ });
507
+ ys.push(1);
508
+ `,
509
+ },
510
+ ],
511
+ },
512
+ ],
513
+ },
514
+ {
515
+ name: 'only suggests when the parameter is annotated, since the loop variable cannot be',
516
+ code: dedent`
517
+ declare const xs: readonly number[];
518
+ const ys = xs.flatMap((x: number) => (x > 0 ? [x] : []));
519
+ `,
520
+ output: null,
521
+ errors: [
522
+ {
523
+ messageId: 'useArrGenerate',
524
+ suggestions: [
525
+ {
526
+ messageId: 'suggestArrGenerate',
527
+ output: dedent`
528
+ import { Arr } from 'ts-data-forge';
529
+ declare const xs: readonly number[];
530
+ const ys = Arr.generate(function* () {
531
+ for (const x of xs) {
532
+ if (x > 0) {
533
+ yield x;
534
+ }
535
+ }
536
+ });
537
+ `,
538
+ },
539
+ ],
540
+ },
541
+ ],
542
+ },
543
+ {
544
+ name: 'only suggests when the callback declares its return type, which may be what types the elements',
545
+ code: dedent`
546
+ declare const xs: readonly number[];
547
+ const ys = xs.flatMap((x): readonly (number | string)[] =>
548
+ x > 0 ? [x] : [],
549
+ );
550
+ `,
551
+ output: null,
552
+ errors: [
553
+ {
554
+ messageId: 'useArrGenerate',
555
+ suggestions: [
556
+ {
557
+ messageId: 'suggestArrGenerate',
558
+ output: dedent`
559
+ import { Arr } from 'ts-data-forge';
560
+ declare const xs: readonly number[];
561
+ const ys = Arr.generate(function* () {
562
+ for (const x of xs) {
563
+ if (x > 0) {
564
+ yield x;
565
+ }
566
+ }
567
+ });
568
+ `,
569
+ },
570
+ ],
571
+ },
572
+ ],
573
+ },
574
+ {
575
+ name: 'only suggests for a filter() receiver, which the loop would iterate as an intermediate array',
576
+ code: dedent`
577
+ declare const xs: readonly number[];
578
+ const ys = xs.filter((x) => x > 0).flatMap((x) => (x > 1 ? [x] : []));
579
+ `,
580
+ output: null,
581
+ errors: [
582
+ {
583
+ messageId: 'useArrGenerate',
584
+ suggestions: [
585
+ {
586
+ messageId: 'suggestArrGenerate',
587
+ output: dedent`
588
+ import { Arr } from 'ts-data-forge';
589
+ declare const xs: readonly number[];
590
+ const ys = Arr.generate(function* () {
591
+ for (const x of xs.filter((x) => x > 0)) {
592
+ if (x > 1) {
593
+ yield x;
594
+ }
595
+ }
596
+ });
597
+ `,
598
+ },
599
+ ],
600
+ },
601
+ ],
602
+ },
603
+ {
604
+ name: 'only suggests when the receiver relies on a narrowing that does not reach into the generator function',
605
+ code: dedent`
606
+ type Decl = Readonly<{ items: readonly number[] }>;
607
+ declare const s: Readonly<{ decl: Decl | null }>;
608
+ let mut_xs: readonly number[] | undefined = [];
609
+ export const f = (): readonly number[] => {
610
+ if (s.decl === null || mut_xs === undefined) {
611
+ return [];
612
+ }
613
+ const a = s.decl.items.flatMap((x) => (x > 0 ? [x] : []));
614
+ const b = mut_xs.flatMap((x) => (x > 0 ? [x] : []));
615
+ return [...a, ...b];
616
+ };
617
+ mut_xs = undefined;
618
+ `,
619
+ output: null,
620
+ errors: [
621
+ {
622
+ messageId: 'useArrGenerate',
623
+ suggestions: [
624
+ {
625
+ messageId: 'suggestArrGenerate',
626
+ output: dedent`
627
+ import { Arr } from 'ts-data-forge';
628
+ type Decl = Readonly<{ items: readonly number[] }>;
629
+ declare const s: Readonly<{ decl: Decl | null }>;
630
+ let mut_xs: readonly number[] | undefined = [];
631
+ export const f = (): readonly number[] => {
632
+ if (s.decl === null || mut_xs === undefined) {
633
+ return [];
634
+ }
635
+ const a = Arr.generate(function* () {
636
+ for (const x of s.decl.items) {
637
+ if (x > 0) {
638
+ yield x;
639
+ }
640
+ }
641
+ });
642
+ const b = mut_xs.flatMap((x) => (x > 0 ? [x] : []));
643
+ return [...a, ...b];
644
+ };
645
+ mut_xs = undefined;
646
+ `,
647
+ },
648
+ ],
649
+ },
650
+ {
651
+ messageId: 'useArrGenerate',
652
+ suggestions: [
653
+ {
654
+ messageId: 'suggestArrGenerate',
655
+ output: dedent`
656
+ import { Arr } from 'ts-data-forge';
657
+ type Decl = Readonly<{ items: readonly number[] }>;
658
+ declare const s: Readonly<{ decl: Decl | null }>;
659
+ let mut_xs: readonly number[] | undefined = [];
660
+ export const f = (): readonly number[] => {
661
+ if (s.decl === null || mut_xs === undefined) {
662
+ return [];
663
+ }
664
+ const a = s.decl.items.flatMap((x) => (x > 0 ? [x] : []));
665
+ const b = Arr.generate(function* () {
666
+ for (const x of mut_xs) {
667
+ if (x > 0) {
668
+ yield x;
669
+ }
670
+ }
671
+ });
672
+ return [...a, ...b];
673
+ };
674
+ mut_xs = undefined;
675
+ `,
676
+ },
677
+ ],
678
+ },
679
+ ],
680
+ },
681
+ {
682
+ name: 'fixes a receiver narrowed through a const or a parameter that is never reassigned, which the generator function still sees narrowed',
683
+ code: dedent`
684
+ declare const c: readonly number[] | undefined;
685
+ export const f = (p: readonly number[] | undefined): readonly number[] => {
686
+ if (c === undefined || p === undefined) {
687
+ return [];
688
+ }
689
+ const a = c.flatMap((x) => (x > 0 ? [x] : []));
690
+ const b = p.flatMap((x) => (x > 0 ? [x] : []));
691
+ return [...a, ...b];
692
+ };
693
+ `,
694
+ output: dedent`
695
+ import { Arr } from 'ts-data-forge';
696
+ declare const c: readonly number[] | undefined;
697
+ export const f = (p: readonly number[] | undefined): readonly number[] => {
698
+ if (c === undefined || p === undefined) {
699
+ return [];
700
+ }
701
+ const a = Arr.generate(function* () {
702
+ for (const x of c) {
703
+ if (x > 0) {
704
+ yield x;
705
+ }
706
+ }
707
+ });
708
+ const b = Arr.generate(function* () {
709
+ for (const x of p) {
710
+ if (x > 0) {
711
+ yield x;
712
+ }
713
+ }
714
+ });
715
+ return [...a, ...b];
716
+ };
717
+ `,
718
+ errors: [
719
+ { messageId: 'useArrGenerate' },
720
+ { messageId: 'useArrGenerate' },
721
+ ],
722
+ },
723
+ {
724
+ name: 'fixes a receiver whose narrowing only makes it a non-empty tuple of the same elements, and a receiver that calls a method',
725
+ code: dedent`
726
+ declare const isNonEmpty: <T,>(
727
+ xs: readonly T[],
728
+ ) => xs is readonly [T, ...T[]];
729
+ declare const o: Readonly<{ names: readonly string[] }>;
730
+ declare const nested: readonly (readonly string[])[];
731
+ export const f = (): readonly string[] => {
732
+ if (!isNonEmpty(o.names)) {
733
+ return [];
734
+ }
735
+ const a = o.names.flatMap((x) => (x !== '' ? [x] : []));
736
+ const b = nested.flat().flatMap((x) => (x !== '' ? [x] : []));
737
+ return [...a, ...b];
738
+ };
739
+ `,
740
+ output: dedent`
741
+ import { Arr } from 'ts-data-forge';
742
+ declare const isNonEmpty: <T,>(
743
+ xs: readonly T[],
744
+ ) => xs is readonly [T, ...T[]];
745
+ declare const o: Readonly<{ names: readonly string[] }>;
746
+ declare const nested: readonly (readonly string[])[];
747
+ export const f = (): readonly string[] => {
748
+ if (!isNonEmpty(o.names)) {
749
+ return [];
750
+ }
751
+ const a = Arr.generate(function* () {
752
+ for (const x of o.names) {
753
+ if (x !== '') {
754
+ yield x;
755
+ }
756
+ }
757
+ });
758
+ const b = Arr.generate(function* () {
759
+ for (const x of nested.flat()) {
760
+ if (x !== '') {
761
+ yield x;
762
+ }
763
+ }
764
+ });
765
+ return [...a, ...b];
766
+ };
767
+ `,
768
+ errors: [
769
+ { messageId: 'useArrGenerate' },
770
+ { messageId: 'useArrGenerate' },
771
+ ],
772
+ },
773
+ {
774
+ name: 'only suggests when a comment would be lost',
775
+ code: dedent`
776
+ declare const xs: readonly number[];
777
+ const ys = xs.flatMap((x) =>
778
+ // keep the positives
779
+ x > 0 ? [x] : [],
780
+ );
781
+ `,
782
+ output: null,
783
+ errors: [
784
+ {
785
+ messageId: 'useArrGenerate',
786
+ suggestions: [
787
+ {
788
+ messageId: 'suggestArrGenerate',
789
+ output: dedent`
790
+ import { Arr } from 'ts-data-forge';
791
+ declare const xs: readonly number[];
792
+ const ys = Arr.generate(function* () {
793
+ for (const x of xs) {
794
+ if (x > 0) {
795
+ yield x;
796
+ }
797
+ }
798
+ });
799
+ `,
800
+ },
801
+ ],
802
+ },
803
+ ],
804
+ },
805
+ ],
806
+ });
807
+ });
808
+
809
+ describe('prefer-arr-generate through type wrappers', () => {
810
+ tester.run('prefer-arr-generate', preferArrGenerate, {
811
+ valid: [
812
+ {
813
+ name: 'an array widened by `as` to a union with something that is not one',
814
+ code: dedent`
815
+ declare const xs: readonly number[];
816
+ const ys = (
817
+ xs as
818
+ | readonly number[]
819
+ | Readonly<{
820
+ flatMap: (
821
+ f: (x: number) => readonly number[],
822
+ ) => readonly number[];
823
+ }>
824
+ ).flatMap((x) => (x > 0 ? [x] : []));
825
+ `,
826
+ },
827
+ {
828
+ name: 'an array asserted onto something that is not one',
829
+ code: dedent`
830
+ declare const o: Readonly<{
831
+ flatMap: (f: (x: number) => readonly number[]) => readonly number[];
832
+ }>;
833
+ const ys = (o as unknown as readonly number[]).flatMap((x) =>
834
+ x > 0 ? [x] : [],
835
+ );
836
+ `,
837
+ },
838
+ ],
839
+ invalid: [
840
+ {
841
+ name: 'an array widened by `as` to another array is still one at every layer',
842
+ code: dedent`
843
+ declare const xs: readonly number[];
844
+ const ys = (xs as readonly (number | string)[]).flatMap((x) =>
845
+ x !== '' ? [x] : [],
846
+ );
847
+ `,
848
+ output: dedent`
849
+ import { Arr } from 'ts-data-forge';
850
+ declare const xs: readonly number[];
851
+ const ys = Arr.generate(function* () {
852
+ for (const x of xs as readonly (number | string)[]) {
853
+ if (x !== '') {
854
+ yield x;
855
+ }
856
+ }
857
+ });
858
+ `,
859
+ errors: [{ messageId: 'useArrGenerate' }],
860
+ },
861
+ {
862
+ name: 'a wrapped receiver or method keeps its wrapper',
863
+ code: dedent`
864
+ declare const xs: readonly number[];
865
+ const a = (xs satisfies readonly number[]).flatMap((x) =>
866
+ x > 0 ? [x] : [],
867
+ );
868
+ const b = xs.flatMap!((x) => (x > 0 ? [x] : []));
869
+ `,
870
+ output: dedent`
871
+ import { Arr } from 'ts-data-forge';
872
+ declare const xs: readonly number[];
873
+ const a = Arr.generate(function* () {
874
+ for (const x of xs satisfies readonly number[]) {
875
+ if (x > 0) {
876
+ yield x;
877
+ }
878
+ }
879
+ });
880
+ const b = Arr.generate(function* () {
881
+ for (const x of xs) {
882
+ if (x > 0) {
883
+ yield x;
884
+ }
885
+ }
886
+ });
887
+ `,
888
+ errors: [
889
+ { messageId: 'useArrGenerate' },
890
+ { messageId: 'useArrGenerate' },
891
+ ],
892
+ },
893
+ {
894
+ name: 'a wrapped condition or element keeps its wrapper',
895
+ code: dedent`
896
+ declare const xs: readonly (number | undefined)[];
897
+ const ys = xs.flatMap((x) =>
898
+ ((x !== undefined) satisfies boolean) ? [x!] : [],
899
+ );
900
+ `,
901
+ output: dedent`
902
+ import { Arr } from 'ts-data-forge';
903
+ declare const xs: readonly (number | undefined)[];
904
+ const ys = Arr.generate(function* () {
905
+ for (const x of xs) {
906
+ if ((x !== undefined) satisfies boolean) {
907
+ yield x!;
908
+ }
909
+ }
910
+ });
911
+ `,
912
+ errors: [{ messageId: 'useArrGenerate' }],
913
+ },
914
+ {
915
+ name: 'a wrapped branch or conditional, which may be what types the result, is only suggested',
916
+ code: dedent`
917
+ declare const xs: readonly number[];
918
+ const a = xs.flatMap((x) => (x > 0 ? [x] : ([] as readonly string[])));
919
+ const b = xs.flatMap((x) => (x > 0 ? [x] : []) as readonly number[]);
920
+ `,
921
+ output: null,
922
+ errors: [
923
+ {
924
+ messageId: 'useArrGenerate',
925
+ suggestions: [
926
+ {
927
+ messageId: 'suggestArrGenerate',
928
+ output: dedent`
929
+ import { Arr } from 'ts-data-forge';
930
+ declare const xs: readonly number[];
931
+ const a = Arr.generate(function* () {
932
+ for (const x of xs) {
933
+ if (x > 0) {
934
+ yield x;
935
+ }
936
+ }
937
+ });
938
+ const b = xs.flatMap((x) => (x > 0 ? [x] : []) as readonly number[]);
939
+ `,
940
+ },
941
+ ],
942
+ },
943
+ {
944
+ messageId: 'useArrGenerate',
945
+ suggestions: [
946
+ {
947
+ messageId: 'suggestArrGenerate',
948
+ output: dedent`
949
+ import { Arr } from 'ts-data-forge';
950
+ declare const xs: readonly number[];
951
+ const a = xs.flatMap((x) => (x > 0 ? [x] : ([] as readonly string[])));
952
+ const b = Arr.generate(function* () {
953
+ for (const x of xs) {
954
+ if (x > 0) {
955
+ yield x;
956
+ }
957
+ }
958
+ });
959
+ `,
960
+ },
961
+ ],
962
+ },
963
+ ],
964
+ },
965
+ ],
966
+ });
967
+ });