eslint-plugin-ts-data-forge 0.10.4 → 0.10.6

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 (93) hide show
  1. package/dist/rules/ast-utils.d.mts +5 -5
  2. package/dist/rules/ast-utils.d.mts.map +1 -1
  3. package/dist/rules/ast-utils.mjs +3 -3
  4. package/dist/rules/ast-utils.mjs.map +1 -1
  5. package/dist/rules/import-binding.d.mts +38 -0
  6. package/dist/rules/import-binding.d.mts.map +1 -0
  7. package/dist/rules/import-binding.mjs +53 -0
  8. package/dist/rules/import-binding.mjs.map +1 -0
  9. package/dist/rules/mutable-result.mjs +37 -37
  10. package/dist/rules/mutable-result.mjs.map +1 -1
  11. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  12. package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
  13. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  14. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  15. package/dist/rules/prefer-arr-generate.mjs +129 -131
  16. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  18. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
  19. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  21. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
  22. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  23. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  24. package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
  25. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  26. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  27. package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
  28. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  29. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
  31. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  33. package/dist/rules/prefer-arr-scan.mjs +9 -18
  34. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  35. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  36. package/dist/rules/prefer-arr-sum.mjs +16 -21
  37. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  38. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  39. package/dist/rules/prefer-arr-uniq.mjs +38 -43
  40. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  41. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  42. package/dist/rules/prefer-as-int.mjs +19 -26
  43. package/dist/rules/prefer-as-int.mjs.map +1 -1
  44. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  45. package/dist/rules/prefer-canonical-array-slicing.mjs +49 -15
  46. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  47. package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
  48. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  49. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  50. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
  51. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  52. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
  53. package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
  54. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  55. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  56. package/dist/rules/prefer-range-for-loop.mjs +18 -24
  57. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  58. package/package.json +5 -5
  59. package/src/rules/ast-utils.mts +11 -11
  60. package/src/rules/import-binding.mts +94 -0
  61. package/src/rules/mutable-result.mts +56 -56
  62. package/src/rules/no-unnecessary-type-guard.mts +199 -203
  63. package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
  64. package/src/rules/prefer-arr-generate.mts +184 -194
  65. package/src/rules/prefer-arr-generate.test.mts +262 -134
  66. package/src/rules/prefer-arr-is-array.test.mts +87 -5
  67. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
  68. package/src/rules/prefer-arr-is-fixed-length-array.mts +24 -36
  69. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
  70. package/src/rules/prefer-arr-is-max-length-array.mts +21 -34
  71. package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
  72. package/src/rules/prefer-arr-is-min-length-array.mts +21 -34
  73. package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
  74. package/src/rules/prefer-arr-is-non-empty.mts +17 -34
  75. package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
  76. package/src/rules/prefer-arr-scan.mts +11 -36
  77. package/src/rules/prefer-arr-scan.test.mts +55 -0
  78. package/src/rules/prefer-arr-sum.mts +19 -33
  79. package/src/rules/prefer-arr-uniq.mts +49 -60
  80. package/src/rules/prefer-arr-uniq.test.mts +46 -20
  81. package/src/rules/prefer-as-int.mts +27 -52
  82. package/src/rules/prefer-as-int.test.mts +79 -29
  83. package/src/rules/prefer-canonical-array-slicing.mts +86 -24
  84. package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
  85. package/src/rules/prefer-canonical-length-cast.mts +18 -18
  86. package/src/rules/prefer-is-non-null-object.test.mts +62 -37
  87. package/src/rules/prefer-is-record-and-has-key.test.mts +32 -17
  88. package/src/rules/prefer-num-safe-parse-float.test.mts +45 -21
  89. package/src/rules/prefer-num-safe-parse-int.test.mts +29 -14
  90. package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
  91. package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
  92. package/src/rules/prefer-pipe-map-nullable.mts +6 -8
  93. package/src/rules/prefer-range-for-loop.mts +31 -55
@@ -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: {
@@ -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,13 +49,6 @@ 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();
@@ -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
  {
@@ -3,15 +3,10 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import {
7
- buildNamedImportFix,
8
- getArgumentText,
9
- getImportDeclarations,
10
- getNamedImports,
11
- } from 'eslint-rule-forge';
6
+ import { getArgumentText } from 'eslint-rule-forge';
12
7
  import type { DeepReadonly } from 'ts-type-forge';
13
8
  import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
14
- import { TS_DATA_FORGE_MODULE } from './constants.mjs';
9
+ import { tsDataForgeValueAtByName } from './import-binding.mjs';
15
10
 
16
11
  type Options = readonly [];
17
12
 
@@ -36,13 +31,6 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
36
31
  create: (context) => {
37
32
  const sourceCode = context.sourceCode;
38
33
 
39
- const program = sourceCode.ast;
40
-
41
- const tsDataForgeImports = getImportDeclarations(
42
- program,
43
- TS_DATA_FORGE_MODULE,
44
- );
45
-
46
34
  const mut_nodesToFix: {
47
35
  node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
48
36
  typeName: string;
@@ -71,44 +59,31 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
71
59
  TSAsExpression: collect,
72
60
  TSTypeAssertion: collect,
73
61
  'Program:exit': () => {
74
- const namedImports = getNamedImports(tsDataForgeImports);
75
-
76
- // Group nodes by function name to handle imports efficiently
77
- const functionNameToNodes = Map.groupBy(
78
- mut_nodesToFix,
79
- (nodeInfo) => nodeInfo.functionName,
80
- );
81
-
82
- // Process each group
83
- for (const [functionName, nodes] of functionNameToNodes) {
84
- const hasImport = namedImports.includes(functionName);
85
-
86
- for (const [index, { node, typeName }] of nodes.entries()) {
87
- context.report({
88
- node,
89
- messageId: 'useBrandedNumberCastFunction',
90
- data: {
91
- typeName,
92
- functionName,
93
- },
94
- fix: (fixer) => {
95
- const replacement = `${functionName}(${getArgumentText(sourceCode, node.expression)})`;
96
-
97
- // Add import only for the first node of this function and only if not already imported
98
- const importFixes =
99
- index === 0 && !hasImport
100
- ? buildNamedImportFix(
101
- fixer,
102
- program,
103
- TS_DATA_FORGE_MODULE,
104
- [functionName],
105
- )
106
- : [];
107
-
108
- return [...importFixes, fixer.replaceText(node, replacement)];
109
- },
110
- });
111
- }
62
+ // One binding per cast function, however many casts use it.
63
+ const castAt = tsDataForgeValueAtByName(sourceCode);
64
+
65
+ for (const { node, typeName, functionName } of mut_nodesToFix) {
66
+ const cast = castAt(functionName)(node);
67
+
68
+ context.report({
69
+ node,
70
+ messageId: 'useBrandedNumberCastFunction',
71
+ data: {
72
+ typeName,
73
+ // The name the fix writes: an aliased import's local name.
74
+ functionName: cast?.localName ?? functionName,
75
+ },
76
+ fix:
77
+ cast === undefined
78
+ ? null
79
+ : (fixer) => [
80
+ ...cast.edits(fixer),
81
+ fixer.replaceText(
82
+ node,
83
+ `${cast.localName}(${getArgumentText(sourceCode, node.expression)})`,
84
+ ),
85
+ ],
86
+ });
112
87
  }
113
88
  },
114
89
  };
@@ -60,24 +60,6 @@ describe('prefer-as-int', () => {
60
60
  `,
61
61
  errors: [{ messageId: 'useBrandedNumberCastFunction' }],
62
62
  },
63
- {
64
- name: 'replaces multiple as Int with asInt imports',
65
- code: dedent`
66
- const n = 1;
67
- const result = n as Int;
68
- const another = (n + 1) as Int;
69
- `,
70
- output: dedent`
71
- import { asInt } from 'ts-data-forge';
72
- const n = 1;
73
- const result = asInt(n);
74
- const another = asInt(n + 1);
75
- `,
76
- errors: [
77
- { messageId: 'useBrandedNumberCastFunction' },
78
- { messageId: 'useBrandedNumberCastFunction' },
79
- ],
80
- },
81
63
  // {
82
64
  // name: 'replaces as Int with asInt import with another disabled line',
83
65
  // code: dedent`
@@ -140,7 +122,7 @@ describe('prefer-as-int', () => {
140
122
 
141
123
  ...([
142
124
  {
143
- name: 'adds import even if ts-data-forge import exists',
125
+ name: 'adds the name to an existing ts-data-forge import',
144
126
  code: dedent`
145
127
  import { isNonNullObject } from 'ts-data-forge';
146
128
 
@@ -148,8 +130,7 @@ describe('prefer-as-int', () => {
148
130
  const result = n as Int;
149
131
  `,
150
132
  output: dedent`
151
- import { asInt } from 'ts-data-forge';
152
- import { isNonNullObject } from 'ts-data-forge';
133
+ import { isNonNullObject, asInt } from 'ts-data-forge';
153
134
 
154
135
  const n = 1;
155
136
  const result = asInt(n);
@@ -157,7 +138,7 @@ describe('prefer-as-int', () => {
157
138
  errors: [{ messageId: 'useBrandedNumberCastFunction' }],
158
139
  },
159
140
  {
160
- name: 'adds import even if ts-data-forge import exists (with other imports)',
141
+ name: 'adds the name to an existing ts-data-forge import (with other imports)',
161
142
  code: dedent`
162
143
  import { noop } from './noop.mjs';
163
144
  import { isNonNullObject } from 'ts-data-forge';
@@ -166,9 +147,8 @@ describe('prefer-as-int', () => {
166
147
  const result = n as Int;
167
148
  `,
168
149
  output: dedent`
169
- import { asInt } from 'ts-data-forge';
170
150
  import { noop } from './noop.mjs';
171
- import { isNonNullObject } from 'ts-data-forge';
151
+ import { isNonNullObject, asInt } from 'ts-data-forge';
172
152
 
173
153
  const n = 1;
174
154
  const result = asInt(n);
@@ -183,6 +163,32 @@ describe('prefer-as-int', () => {
183
163
  tester.run('prefer-as-int (other types)', preferAsInt, {
184
164
  valid: [],
185
165
  invalid: [
166
+ {
167
+ name: 'replaces multiple as Int with asInt imports',
168
+ code: dedent`
169
+ const n = 1;
170
+ const result = n as Int;
171
+ const another = (n + 1) as Int;
172
+ `,
173
+ output: [
174
+ dedent`
175
+ import { asInt } from 'ts-data-forge';
176
+ const n = 1;
177
+ const result = asInt(n);
178
+ const another = (n + 1) as Int;
179
+ `,
180
+ dedent`
181
+ import { asInt } from 'ts-data-forge';
182
+ const n = 1;
183
+ const result = asInt(n);
184
+ const another = asInt(n + 1);
185
+ `,
186
+ ],
187
+ errors: [
188
+ { messageId: 'useBrandedNumberCastFunction' },
189
+ { messageId: 'useBrandedNumberCastFunction' },
190
+ ],
191
+ },
186
192
  {
187
193
  name: 'replaces as Uint32 with asUint32',
188
194
  code: dedent`
@@ -239,17 +245,14 @@ describe('prefer-as-int', () => {
239
245
  const c = n as SafeInt;
240
246
  `,
241
247
  dedent`
242
- import { asUint32 } from 'ts-data-forge';
243
- import { asInt } from 'ts-data-forge';
248
+ import { asInt, asUint32 } from 'ts-data-forge';
244
249
  const n = 1;
245
250
  const a = asInt(n);
246
251
  const b = asUint32(n);
247
252
  const c = n as SafeInt;
248
253
  `,
249
254
  dedent`
250
- import { asSafeInt } from 'ts-data-forge';
251
- import { asUint32 } from 'ts-data-forge';
252
- import { asInt } from 'ts-data-forge';
255
+ import { asInt, asUint32, asSafeInt } from 'ts-data-forge';
253
256
  const n = 1;
254
257
  const a = asInt(n);
255
258
  const b = asUint32(n);
@@ -316,3 +319,50 @@ describe('prefer-as-int with parenthesized operands', () => {
316
319
  ],
317
320
  });
318
321
  }, 20000);
322
+
323
+ describe('prefer-as-int and the import it adds', () => {
324
+ tester.run('prefer-as-int', preferAsInt, {
325
+ valid: [],
326
+ invalid: [
327
+ {
328
+ name: 'writes the local name of an aliased import',
329
+ code: dedent`
330
+ import { asInt as toInt } from 'ts-data-forge';
331
+ const n = 1;
332
+ const result = n as Int;
333
+ `,
334
+ output: dedent`
335
+ import { asInt as toInt } from 'ts-data-forge';
336
+ const n = 1;
337
+ const result = toInt(n);
338
+ `,
339
+ errors: [
340
+ {
341
+ messageId: 'useBrandedNumberCastFunction',
342
+ data: { typeName: 'Int', functionName: 'toInt' },
343
+ },
344
+ ],
345
+ },
346
+ {
347
+ name: 'no fix when an import type binds the name',
348
+ code: dedent`
349
+ import type { asInt } from 'ts-data-forge';
350
+ const n = 1;
351
+ const result = n as Int;
352
+ `,
353
+ output: null,
354
+ errors: [{ messageId: 'useBrandedNumberCastFunction' }],
355
+ },
356
+ {
357
+ name: 'no fix where a local binding takes the name',
358
+ code: dedent`
359
+ const asInt = (x: number): number => x;
360
+ const n = 1;
361
+ const result = n as Int;
362
+ `,
363
+ output: null,
364
+ errors: [{ messageId: 'useBrandedNumberCastFunction' }],
365
+ },
366
+ ],
367
+ });
368
+ }, 20000);