eslint-plugin-ts-data-forge 0.10.5 → 0.10.7

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 (107) hide show
  1. package/README.md +3 -3
  2. package/dist/plugin.d.mts +2 -2
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +5 -5
  7. package/dist/rules/ast-utils.d.mts +8 -9
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +3 -4
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/import-binding.d.mts +38 -0
  12. package/dist/rules/import-binding.d.mts.map +1 -0
  13. package/dist/rules/import-binding.mjs +53 -0
  14. package/dist/rules/import-binding.mjs.map +1 -0
  15. package/dist/rules/mutable-result.mjs +37 -37
  16. package/dist/rules/mutable-result.mjs.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 +146 -143
  19. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-generate.d.mts +2 -2
  21. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-generate.mjs +146 -136
  23. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +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 +15 -17
  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.map +1 -1
  31. package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
  32. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  33. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
  35. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  37. package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
  38. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  39. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  40. package/dist/rules/prefer-arr-scan.mjs +9 -18
  41. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  42. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  43. package/dist/rules/prefer-arr-sum.mjs +16 -21
  44. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  45. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  46. package/dist/rules/prefer-arr-uniq.mjs +38 -43
  47. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  48. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  49. package/dist/rules/prefer-as-int.mjs +19 -26
  50. package/dist/rules/prefer-as-int.mjs.map +1 -1
  51. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  52. package/dist/rules/prefer-canonical-array-slicing.mjs +49 -16
  53. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  54. package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
  55. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  56. package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -1
  57. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  58. package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
  59. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  60. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  61. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
  62. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  63. package/dist/rules/prefer-pipe-map-nullable.d.mts +2 -2
  64. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
  65. package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
  66. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  67. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  68. package/dist/rules/prefer-range-for-loop.mjs +18 -24
  69. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  70. package/dist/rules/rules.d.mts +2 -2
  71. package/package.json +6 -6
  72. package/src/plugin.mts +4 -3
  73. package/src/rule-types.mts +5 -5
  74. package/src/rules/ast-utils.mts +14 -15
  75. package/src/rules/import-binding.mts +94 -0
  76. package/src/rules/mutable-result.mts +56 -56
  77. package/src/rules/no-unnecessary-type-guard.mts +203 -207
  78. package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
  79. package/src/rules/prefer-arr-generate.mts +222 -214
  80. package/src/rules/prefer-arr-generate.test.mts +490 -133
  81. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
  82. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  83. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
  84. package/src/rules/prefer-arr-is-max-length-array.mts +23 -36
  85. package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
  86. package/src/rules/prefer-arr-is-min-length-array.mts +23 -36
  87. package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
  88. package/src/rules/prefer-arr-is-non-empty.mts +17 -34
  89. package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
  90. package/src/rules/prefer-arr-scan.mts +17 -42
  91. package/src/rules/prefer-arr-scan.test.mts +55 -0
  92. package/src/rules/prefer-arr-sum.mts +22 -36
  93. package/src/rules/prefer-arr-uniq.mts +51 -62
  94. package/src/rules/prefer-arr-uniq.test.mts +46 -20
  95. package/src/rules/prefer-as-int.mts +27 -52
  96. package/src/rules/prefer-as-int.test.mts +79 -29
  97. package/src/rules/prefer-canonical-array-slicing.mts +101 -40
  98. package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
  99. package/src/rules/prefer-canonical-length-cast.mts +29 -29
  100. package/src/rules/prefer-canonical-length-guard.mts +2 -2
  101. package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
  102. package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
  103. package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
  104. package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
  105. package/src/rules/prefer-pipe-map-nullable.mts +17 -19
  106. package/src/rules/prefer-range-for-loop.mts +37 -61
  107. package/src/rules/prefer-range-for-loop.test.mts +2 -2
@@ -167,3 +167,113 @@ describe('prefer-arr-is-non-empty with parenthesized operands', () => {
167
167
  ],
168
168
  });
169
169
  }, 20000);
170
+
171
+ describe('prefer-arr-is-non-empty and the import it adds', () => {
172
+ tester.run('prefer-arr-is-non-empty', preferArrIsNonEmpty, {
173
+ valid: [],
174
+ invalid: [
175
+ {
176
+ name: 'a leading directive stays first',
177
+ code: dedent`
178
+ 'use client';
179
+ declare const xs: readonly number[];
180
+ const ok = xs.length > 0;
181
+ `,
182
+ output: dedent`
183
+ 'use client';
184
+ import { Arr } from 'ts-data-forge';
185
+ declare const xs: readonly number[];
186
+ const ok = Arr.isNonEmpty(xs);
187
+ `,
188
+ errors: [{ messageId: 'useIsNonEmpty' }],
189
+ },
190
+ {
191
+ name: 'joins an existing named import from the module',
192
+ code: dedent`
193
+ import { asInt } from 'ts-data-forge';
194
+ declare const xs: readonly number[];
195
+ const ok = xs.length > 0;
196
+ `,
197
+ output: dedent`
198
+ import { asInt, Arr } from 'ts-data-forge';
199
+ declare const xs: readonly number[];
200
+ const ok = Arr.isNonEmpty(xs);
201
+ `,
202
+ errors: [{ messageId: 'useIsNonEmpty' }],
203
+ },
204
+ {
205
+ name: 'turns a type-only specifier into a value import',
206
+ code: dedent`
207
+ import { type Arr } from 'ts-data-forge';
208
+ declare const xs: readonly number[];
209
+ const ok = xs.length > 0;
210
+ `,
211
+ output: dedent`
212
+ import { Arr } from 'ts-data-forge';
213
+ declare const xs: readonly number[];
214
+ const ok = Arr.isNonEmpty(xs);
215
+ `,
216
+ errors: [{ messageId: 'useIsNonEmpty' }],
217
+ },
218
+ {
219
+ name: 'writes the local name of an aliased import',
220
+ code: dedent`
221
+ import { Arr as A } from 'ts-data-forge';
222
+ declare const xs: readonly number[];
223
+ const ok = xs.length > 0;
224
+ `,
225
+ output: dedent`
226
+ import { Arr as A } from 'ts-data-forge';
227
+ declare const xs: readonly number[];
228
+ const ok = A.isNonEmpty(xs);
229
+ `,
230
+ errors: [{ messageId: 'useIsNonEmpty' }],
231
+ },
232
+ {
233
+ name: 'no fix when an import type binds the name',
234
+ code: dedent`
235
+ import type { Arr } from 'ts-data-forge';
236
+ declare const xs: readonly number[];
237
+ const ok = xs.length > 0;
238
+ `,
239
+ output: null,
240
+ errors: [{ messageId: 'useIsNonEmpty' }],
241
+ },
242
+ {
243
+ name: 'no fix where a local binding takes the name',
244
+ code: dedent`
245
+ declare const xs: readonly number[];
246
+ const f = (Arr: unknown) => xs.length > 0;
247
+ `,
248
+ output: null,
249
+ errors: [{ messageId: 'useIsNonEmpty' }],
250
+ },
251
+ {
252
+ name: 'each fix adds the import it needs, one per pass',
253
+ code: dedent`
254
+ declare const xs: readonly number[];
255
+ const a = xs.length > 0;
256
+ const b = 0 < xs.length;
257
+ `,
258
+ output: [
259
+ dedent`
260
+ import { Arr } from 'ts-data-forge';
261
+ declare const xs: readonly number[];
262
+ const a = Arr.isNonEmpty(xs);
263
+ const b = 0 < xs.length;
264
+ `,
265
+ dedent`
266
+ import { Arr } from 'ts-data-forge';
267
+ declare const xs: readonly number[];
268
+ const a = Arr.isNonEmpty(xs);
269
+ const b = Arr.isNonEmpty(xs);
270
+ `,
271
+ ],
272
+ errors: [
273
+ { messageId: 'useIsNonEmpty' },
274
+ { messageId: 'useIsNonEmpty' },
275
+ ],
276
+ },
277
+ ],
278
+ });
279
+ }, 20000);
@@ -4,17 +4,14 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import {
7
- buildNamedImportFix,
8
7
  getArgumentText,
9
- getImportDeclarations,
10
- getNamedImports,
11
8
  skipTypeWrappers,
12
9
  typeWrapperLayers,
13
10
  } from 'eslint-rule-forge';
14
11
  import { Arr } from 'ts-data-forge';
15
12
  import type { FixedLengthTuple } from 'ts-type-forge';
16
13
  import * as ts from 'typescript';
17
- import { TS_DATA_FORGE_MODULE } from './constants.mjs';
14
+ import { tsDataForgeValueAt } from './import-binding.mjs';
18
15
 
19
16
  type Options = readonly [];
20
17
 
@@ -47,8 +44,9 @@ type SliceCall = Readonly<{
47
44
  type AccumulationCandidate = Readonly<{
48
45
  call: TSESTree.CallExpression;
49
46
  /**
50
- * What the call is replaced with, or `undefined` when the rewrite could not
51
- * be derived and the report carries no fix.
47
+ * What the call is replaced with, after the name `Arr` is bound to
48
+ * (`scan(xs, f, init)`), or `undefined` when the rewrite could not be derived
49
+ * and the report carries no fix.
52
50
  */
53
51
  replacement: string | undefined;
54
52
  }>;
@@ -180,13 +178,6 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
180
178
  create: (context) => {
181
179
  const sourceCode = context.sourceCode;
182
180
 
183
- const program = sourceCode.ast;
184
-
185
- const tsDataForgeImports = getImportDeclarations(
186
- program,
187
- TS_DATA_FORGE_MODULE,
188
- );
189
-
190
181
  const services = sourceCode.parserServices;
191
182
 
192
183
  const checker = services?.program?.getTypeChecker();
@@ -289,12 +280,12 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
289
280
  // `acc.at(-1)` is `T | undefined` only because `at` can go out of bounds,
290
281
  // which a seeded accumulator never does. So once the value `scan` carries
291
282
  // is known not to be nullish, a `?? fallback` guarding the read is dead
292
- // and the fix takes it with the read — leaving it would hand the author an
293
- // unnecessary condition that no rule can fix for them. Every layer of
294
- // type wrappers is asked (see typeWrapperLayers): the value under an
295
- // `as` / `!` is what `scan` carries at run time (`[init!]` may still
296
- // hold `undefined`), and a wider type a wrapper states is the author
297
- // saying it may be nullish.
283
+ // and the fix takes it with the read — leaving it would hand the author
284
+ // an unnecessary condition that no rule can fix for them. Every layer of
285
+ // type wrappers is asked (see typeWrapperLayers): the value under an `as`
286
+ // / `!` is what `scan` carries at run time (`[init!]` may still hold
287
+ // `undefined`), and a wider type a wrapper states is the author saying it
288
+ // may be nullish.
298
289
  const carriesNullish = [seed, element].some((node) =>
299
290
  typeWrapperLayers(node).some(isNullish),
300
291
  );
@@ -363,7 +354,7 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
363
354
  ? `(${elementText})`
364
355
  : elementText;
365
356
 
366
- return `Arr.scan(${arrayText}, (${parameterText}) => ${bodyText}, ${getArgumentText(sourceCode, seed)})`;
357
+ return `scan(${arrayText}, (${parameterText}) => ${bodyText}, ${getArgumentText(sourceCode, seed)})`;
367
358
  };
368
359
 
369
360
  /**
@@ -498,36 +489,20 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
498
489
  // been seen by the time its `map` is judged, whichever order the traversal
499
490
  // reached them in.
500
491
  'Program:exit': () => {
501
- const hasArrImport =
502
- getNamedImports(tsDataForgeImports).includes('Arr');
503
-
504
- // The import is inserted once per pass: two fixes writing the same
505
- // insertion point would otherwise produce it twice.
506
- const mut_importNeededBy = hasArrImport
507
- ? undefined
508
- : mut_accumulations.find(
509
- (accumulation) => accumulation.replacement !== undefined,
510
- )?.call;
492
+ const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
511
493
 
512
- for (const accumulation of mut_accumulations) {
513
- const { call, replacement } = accumulation;
494
+ for (const { call, replacement } of mut_accumulations) {
495
+ const arr = arrAt(call);
514
496
 
515
497
  context.report({
516
498
  node: call,
517
499
  messageId: 'preferArrScanAccumulate',
518
500
  fix:
519
- replacement === undefined
501
+ replacement === undefined || arr === undefined
520
502
  ? undefined
521
503
  : (fixer) => [
522
- ...(call === mut_importNeededBy
523
- ? buildNamedImportFix(
524
- fixer,
525
- program,
526
- TS_DATA_FORGE_MODULE,
527
- ['Arr'],
528
- )
529
- : []),
530
- fixer.replaceText(call, replacement),
504
+ ...arr.edits(fixer),
505
+ fixer.replaceText(call, `${arr.localName}.${replacement}`),
531
506
  ],
532
507
  });
533
508
  }
@@ -442,3 +442,58 @@ describe('prefer-arr-scan with parenthesized operands', () => {
442
442
  ],
443
443
  });
444
444
  });
445
+
446
+ describe('prefer-arr-scan and the import it adds', () => {
447
+ tester.run('prefer-arr-scan', preferArrScan, {
448
+ valid: [],
449
+ invalid: [
450
+ {
451
+ name: 'writes the local name of an aliased import',
452
+ code: dedent`
453
+ import { Arr as A } from 'ts-data-forge';
454
+ declare const xs: readonly number[];
455
+ const totals = xs.reduce<readonly number[]>(
456
+ (acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
457
+ [0],
458
+ );
459
+ `,
460
+ output: dedent`
461
+ import { Arr as A } from 'ts-data-forge';
462
+ declare const xs: readonly number[];
463
+ const totals = A.scan(xs, (acc, x) => (acc) + x, 0);
464
+ `,
465
+ errors: [{ messageId: 'preferArrScanAccumulate' }],
466
+ },
467
+ {
468
+ name: 'turns a type-only specifier into a value import',
469
+ code: dedent`
470
+ import { type Arr } from 'ts-data-forge';
471
+ declare const xs: readonly number[];
472
+ const totals = xs.reduce<readonly number[]>(
473
+ (acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
474
+ [0],
475
+ );
476
+ `,
477
+ output: dedent`
478
+ import { Arr } from 'ts-data-forge';
479
+ declare const xs: readonly number[];
480
+ const totals = Arr.scan(xs, (acc, x) => (acc) + x, 0);
481
+ `,
482
+ errors: [{ messageId: 'preferArrScanAccumulate' }],
483
+ },
484
+ {
485
+ name: 'no fix when an import type binds the name',
486
+ code: dedent`
487
+ import type { Arr } from 'ts-data-forge';
488
+ declare const xs: readonly number[];
489
+ const totals = xs.reduce<readonly number[]>(
490
+ (acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
491
+ [0],
492
+ );
493
+ `,
494
+ output: null,
495
+ errors: [{ messageId: 'preferArrScanAccumulate' }],
496
+ },
497
+ ],
498
+ });
499
+ });
@@ -4,16 +4,13 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import {
7
- buildNamedImportFix,
8
7
  getArgumentText,
9
- getImportDeclarations,
10
- getNamedImports,
11
8
  skipTypeWrappers,
12
9
  typeWrapperLayers,
13
10
  } from 'eslint-rule-forge';
14
11
  import { Arr, pipe } from 'ts-data-forge';
15
12
  import type { TypeReference } from 'typescript';
16
- import { TS_DATA_FORGE_MODULE } from './constants.mjs';
13
+ import { tsDataForgeValueAt } from './import-binding.mjs';
17
14
 
18
15
  type Options = readonly [];
19
16
 
@@ -39,13 +36,6 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
39
36
  create: (context) => {
40
37
  const sourceCode = context.sourceCode;
41
38
 
42
- const program = sourceCode.ast;
43
-
44
- const tsDataForgeImports = getImportDeclarations(
45
- program,
46
- TS_DATA_FORGE_MODULE,
47
- );
48
-
49
39
  const services = context.sourceCode.parserServices;
50
40
 
51
41
  const mut_nodesToFix: {
@@ -77,9 +67,9 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
77
67
 
78
68
  const reducer = node.arguments[0];
79
69
 
80
- // The initial value and the operands of the body are read through
81
- // type wrappers: `0 as number` is still 0, `(b satisfies number)` is
82
- // still `b`. Types are asked of the values underneath too.
70
+ // The initial value and the operands of the body are read through type
71
+ // wrappers: `0 as number` is still 0, `(b satisfies number)` is still
72
+ // `b`. Types are asked of the values underneath too.
83
73
  const initialValue = skipTypeWrappers(node.arguments[1]);
84
74
 
85
75
  // Check initial value is 0
@@ -267,16 +257,16 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
267
257
  }
268
258
  },
269
259
  'Program:exit': () => {
270
- const namedImports = getNamedImports(tsDataForgeImports);
271
-
272
- const hasArrImport = namedImports.includes('Arr');
260
+ const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
273
261
 
274
- for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
262
+ for (const nodeInfo of mut_nodesToFix) {
275
263
  const arrayText = getArgumentText(
276
264
  sourceCode,
277
265
  nodeInfo.arrayExpression,
278
266
  );
279
267
 
268
+ const arr = arrAt(nodeInfo.node);
269
+
280
270
  context.report({
281
271
  node: nodeInfo.node,
282
272
  messageId: nodeInfo.messageId,
@@ -284,24 +274,20 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
284
274
  arrayName: arrayText,
285
275
  mapper: nodeInfo.mapper ?? '',
286
276
  },
287
- fix: (fixer) => {
288
- const replacement =
289
- nodeInfo.messageId === 'useArrSum'
290
- ? `Arr.sum(${arrayText})`
291
- : `Arr.sumBy(${arrayText}, ${nodeInfo.mapper})`;
292
-
293
- const importFixes =
294
- index === 0 && !hasArrImport
295
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
296
- 'Arr',
297
- ])
298
- : [];
299
-
300
- return [
301
- ...importFixes,
302
- fixer.replaceText(nodeInfo.node, replacement),
303
- ];
304
- },
277
+ fix:
278
+ arr === undefined
279
+ ? null
280
+ : (fixer) => {
281
+ const replacement =
282
+ nodeInfo.messageId === 'useArrSum'
283
+ ? `${arr.localName}.sum(${arrayText})`
284
+ : `${arr.localName}.sumBy(${arrayText}, ${nodeInfo.mapper})`;
285
+
286
+ return [
287
+ ...arr.edits(fixer),
288
+ fixer.replaceText(nodeInfo.node, replacement),
289
+ ];
290
+ },
305
291
  });
306
292
  }
307
293
  },
@@ -5,16 +5,14 @@ import {
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
7
  import {
8
- buildNamedImportFix,
9
8
  getArgumentText,
10
- getImportDeclarations,
11
- getImportedLocalName,
12
9
  skipTypeWrappers,
13
10
  typeWrapperLayers,
14
11
  } from 'eslint-rule-forge';
15
12
  import { Arr } from 'ts-data-forge';
16
13
  import * as ts from 'typescript';
17
- import { primitiveTypeFlags, TS_DATA_FORGE_MODULE } from './constants.mjs';
14
+ import { primitiveTypeFlags } from './constants.mjs';
15
+ import { tsDataForgeValueAt } from './import-binding.mjs';
18
16
  import { needsMutableResult } from './mutable-result.mjs';
19
17
 
20
18
  type Options = readonly [];
@@ -51,21 +49,14 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
51
49
  create: (context) => {
52
50
  const sourceCode = context.sourceCode;
53
51
 
54
- const program = sourceCode.ast;
55
-
56
- const tsDataForgeImports = getImportDeclarations(
57
- program,
58
- TS_DATA_FORGE_MODULE,
59
- );
60
-
61
52
  const services = sourceCode.parserServices;
62
53
 
63
54
  const checker = services?.program?.getTypeChecker();
64
55
 
65
56
  const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
66
57
 
67
- // Without type information the rule cannot tell an array of primitives
68
- // from an array of objects, and would suggest code that does not compile.
58
+ // Without type information the rule cannot tell an array of primitives from
59
+ // an array of objects, and would suggest code that does not compile.
69
60
  if (checker === undefined || esTreeNodeToTSNodeMap === undefined) {
70
61
  return {};
71
62
  }
@@ -159,14 +150,7 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
159
150
  check(node, element.argument);
160
151
  },
161
152
  'Program:exit': () => {
162
- const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
163
-
164
- const arrName = arrLocalName ?? 'Arr';
165
-
166
- // The import is added by the first fix only, so that several fixes do
167
- // not insert it at the same position. When the first match is not
168
- // autofixable, the first autofixable one carries it instead.
169
- const firstAutofixable = mut_matches.find((m) => m.autofixable);
153
+ const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
170
154
 
171
155
  for (const match of mut_matches) {
172
156
  const arrayText = sourceCode.getText(match.arrayExpression);
@@ -176,16 +160,18 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
176
160
  match.arrayExpression,
177
161
  );
178
162
 
179
- const buildFix =
180
- (withImport: boolean): TSESLint.ReportFixFunction =>
181
- (fixer) => [
182
- ...(withImport && arrLocalName === undefined
183
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
184
- 'Arr',
185
- ])
186
- : []),
187
- fixer.replaceText(match.node, `${arrName}.uniq(${argumentText})`),
188
- ];
163
+ const arr = arrAt(match.node);
164
+
165
+ const fix: TSESLint.ReportFixFunction | null =
166
+ arr === undefined
167
+ ? null
168
+ : (fixer) => [
169
+ ...arr.edits(fixer),
170
+ fixer.replaceText(
171
+ match.node,
172
+ `${arr.localName}.uniq(${argumentText})`,
173
+ ),
174
+ ];
189
175
 
190
176
  const data = { arrayName: arrayText } as const;
191
177
 
@@ -194,20 +180,23 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
194
180
  node: match.node,
195
181
  messageId: 'useArrUniq',
196
182
  data,
197
- fix: buildFix(match === firstAutofixable),
183
+ fix,
198
184
  });
199
185
  } else {
200
186
  context.report({
201
187
  node: match.node,
202
188
  messageId: 'useArrUniq',
203
189
  data,
204
- suggest: [
205
- {
206
- messageId: 'suggestArrUniq',
207
- data,
208
- fix: buildFix(true),
209
- },
210
- ],
190
+ suggest:
191
+ fix === null
192
+ ? []
193
+ : [
194
+ {
195
+ messageId: 'suggestArrUniq',
196
+ data,
197
+ fix,
198
+ },
199
+ ],
211
200
  });
212
201
  }
213
202
  }
@@ -257,6 +246,29 @@ const isGlobal = (
257
246
  return variable === null || variable.defs.length === 0;
258
247
  };
259
248
 
249
+ const resolveConstraint = (
250
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
251
+ checker: ts.TypeChecker,
252
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
253
+ type: ts.Type,
254
+ ): ts.Type =>
255
+ (type.flags & ts.TypeFlags.TypeParameter) !== 0
256
+ ? (checker.getBaseConstraintOfType(type) ?? type)
257
+ : type;
258
+
259
+ const isPrimitiveType = (
260
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
261
+ checker: ts.TypeChecker,
262
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
263
+ type: ts.Type,
264
+ ): boolean => {
265
+ const resolved = resolveConstraint(checker, type);
266
+
267
+ return resolved.isUnion()
268
+ ? resolved.types.every((t) => isPrimitiveType(checker, t))
269
+ : (resolved.flags & primitiveTypeFlags) !== 0;
270
+ };
271
+
260
272
  /**
261
273
  * Whether `type` is an array or tuple (or a union or intersection of them) whose elements are
262
274
  * all primitives, i.e. something `Arr.uniq` accepts.
@@ -291,26 +303,3 @@ const isPrimitiveArrayType = (
291
303
  // An empty tuple has nothing to deduplicate, but is still accepted.
292
304
  return typeArguments.every((t) => isPrimitiveType(checker, t));
293
305
  };
294
-
295
- const isPrimitiveType = (
296
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
297
- checker: ts.TypeChecker,
298
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
299
- type: ts.Type,
300
- ): boolean => {
301
- const resolved = resolveConstraint(checker, type);
302
-
303
- return resolved.isUnion()
304
- ? resolved.types.every((t) => isPrimitiveType(checker, t))
305
- : (resolved.flags & primitiveTypeFlags) !== 0;
306
- };
307
-
308
- const resolveConstraint = (
309
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
310
- checker: ts.TypeChecker,
311
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
312
- type: ts.Type,
313
- ): ts.Type =>
314
- (type.flags & ts.TypeFlags.TypeParameter) !== 0
315
- ? (checker.getBaseConstraintOfType(type) ?? type)
316
- : type;
@@ -188,12 +188,20 @@ describe('prefer-arr-uniq', () => {
188
188
  const ys = [...new Set(xs)];
189
189
  const zs = Array.from(new Set(xs));
190
190
  `,
191
- output: dedent`
192
- import { Arr } from 'ts-data-forge';
193
- const xs = ['a', 'b'];
194
- const ys = Arr.uniq(xs);
195
- const zs = Arr.uniq(xs);
196
- `,
191
+ output: [
192
+ dedent`
193
+ import { Arr } from 'ts-data-forge';
194
+ const xs = ['a', 'b'];
195
+ const ys = Arr.uniq(xs);
196
+ const zs = Array.from(new Set(xs));
197
+ `,
198
+ dedent`
199
+ import { Arr } from 'ts-data-forge';
200
+ const xs = ['a', 'b'];
201
+ const ys = Arr.uniq(xs);
202
+ const zs = Arr.uniq(xs);
203
+ `,
204
+ ],
197
205
  errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
198
206
  },
199
207
  {
@@ -205,14 +213,24 @@ describe('prefer-arr-uniq', () => {
205
213
  const b = [...new Set(u)];
206
214
  const c = Array.from(new Set([1, 2, 1]));
207
215
  `,
208
- output: dedent`
209
- import { Arr } from 'ts-data-forge';
210
- const t = [1, 'a', 1] as const;
211
- const u: readonly ('x' | 'y' | null | undefined | boolean | bigint)[] = [];
212
- const a = Arr.uniq(t);
213
- const b = Arr.uniq(u);
214
- const c = Arr.uniq([1, 2, 1]);
215
- `,
216
+ output: [
217
+ dedent`
218
+ import { Arr } from 'ts-data-forge';
219
+ const t = [1, 'a', 1] as const;
220
+ const u: readonly ('x' | 'y' | null | undefined | boolean | bigint)[] = [];
221
+ const a = Arr.uniq(t);
222
+ const b = [...new Set(u)];
223
+ const c = Array.from(new Set([1, 2, 1]));
224
+ `,
225
+ dedent`
226
+ import { Arr } from 'ts-data-forge';
227
+ const t = [1, 'a', 1] as const;
228
+ const u: readonly ('x' | 'y' | null | undefined | boolean | bigint)[] = [];
229
+ const a = Arr.uniq(t);
230
+ const b = Arr.uniq(u);
231
+ const c = Arr.uniq([1, 2, 1]);
232
+ `,
233
+ ],
216
234
  errors: [
217
235
  { messageId: 'useArrUniq' },
218
236
  { messageId: 'useArrUniq' },
@@ -465,12 +483,20 @@ describe('prefer-arr-uniq through type wrappers', () => {
465
483
  const a = Array.from(new Set(xs) as ReadonlySet<string>);
466
484
  const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
467
485
  `,
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
- `,
486
+ output: [
487
+ dedent`
488
+ import { Arr } from 'ts-data-forge';
489
+ declare const xs: readonly string[];
490
+ const a = Arr.uniq(xs);
491
+ const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
492
+ `,
493
+ dedent`
494
+ import { Arr } from 'ts-data-forge';
495
+ declare const xs: readonly string[];
496
+ const a = Arr.uniq(xs);
497
+ const b = Arr.uniq(xs);
498
+ `,
499
+ ],
474
500
  errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
475
501
  },
476
502
  {