eslint-plugin-ts-data-forge 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/plugin.d.mts +1 -1
  2. package/dist/plugin.d.mts.map +1 -1
  3. package/dist/plugin.mjs +1 -0
  4. package/dist/plugin.mjs.map +1 -1
  5. package/dist/rule-types.d.mts +3 -3
  6. package/dist/rule-types.d.mts.map +1 -1
  7. package/dist/rules/ast-utils.d.mts +59 -3
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +142 -31
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/branded-number-types.mjs.map +1 -1
  12. package/dist/rules/import-utils.d.mts +1 -1
  13. package/dist/rules/import-utils.d.mts.map +1 -1
  14. package/dist/rules/import-utils.mjs +26 -13
  15. package/dist/rules/import-utils.mjs.map +1 -1
  16. package/dist/rules/no-side-effect-import.d.mts +1 -1
  17. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  19. package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
  20. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  21. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-is-array.mjs +25 -11
  23. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +11 -21
  27. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
  29. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +16 -28
  31. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
  33. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-max-length-array.mjs +16 -24
  35. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
  37. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-min-length-array.mjs +16 -24
  39. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -22
  42. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-scan.d.mts +6 -5
  44. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  45. package/dist/rules/prefer-arr-scan.mjs +68 -31
  46. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  47. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  48. package/dist/rules/prefer-arr-sum.mjs +57 -40
  49. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  50. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  51. package/dist/rules/prefer-arr-uniq.mjs +59 -30
  52. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  53. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  54. package/dist/rules/prefer-as-int.mjs +27 -24
  55. package/dist/rules/prefer-as-int.mjs.map +1 -1
  56. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  57. package/dist/rules/prefer-canonical-array-slicing.mjs +59 -28
  58. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  59. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  60. package/dist/rules/prefer-canonical-length-cast.mjs +41 -16
  61. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  62. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  63. package/dist/rules/prefer-canonical-length-guard.mjs +18 -8
  64. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  65. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  66. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +31 -6
  67. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  68. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  69. package/dist/rules/prefer-is-non-null-object.mjs +56 -40
  70. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  71. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  72. package/dist/rules/prefer-is-record-and-has-key.mjs +18 -8
  73. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  74. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  75. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
  77. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  78. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
  80. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  81. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  84. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.mjs +15 -8
  86. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  87. package/dist/types.d.mts +2 -2
  88. package/dist/types.d.mts.map +1 -1
  89. package/package.json +25 -23
  90. package/src/plugin.mts +5 -4
  91. package/src/plugin.test.mts +0 -13
  92. package/src/rule-types.mts +3 -3
  93. package/src/rules/ast-utils.mts +192 -37
  94. package/src/rules/branded-number-types.mts +1 -1
  95. package/src/rules/import-utils.mts +41 -17
  96. package/src/rules/no-side-effect-import.mts +1 -1
  97. package/src/rules/no-unnecessary-type-guard.mts +249 -148
  98. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  99. package/src/rules/prefer-arr-is-array.mts +32 -13
  100. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  101. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
  102. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  103. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  104. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  105. package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
  106. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  107. package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
  108. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  109. package/src/rules/prefer-arr-is-non-empty.mts +19 -24
  110. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  111. package/src/rules/prefer-arr-scan.mts +107 -37
  112. package/src/rules/prefer-arr-scan.test.mts +149 -0
  113. package/src/rules/prefer-arr-sum.mts +87 -50
  114. package/src/rules/prefer-arr-sum.test.mts +97 -0
  115. package/src/rules/prefer-arr-uniq.mts +83 -33
  116. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  117. package/src/rules/prefer-as-int.mts +38 -38
  118. package/src/rules/prefer-as-int.test.mts +51 -0
  119. package/src/rules/prefer-canonical-array-slicing.mts +84 -29
  120. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  121. package/src/rules/prefer-canonical-length-cast.mts +56 -18
  122. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  123. package/src/rules/prefer-canonical-length-guard.mts +25 -11
  124. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  125. package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
  126. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  127. package/src/rules/prefer-is-non-null-object.mts +57 -33
  128. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  129. package/src/rules/prefer-is-record-and-has-key.mts +27 -12
  130. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  131. package/src/rules/prefer-num-safe-parse-float.mts +37 -13
  132. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  133. package/src/rules/prefer-num-safe-parse-int.mts +41 -15
  134. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  135. package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
  136. package/src/rules/prefer-range-for-loop.mts +21 -9
  137. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  138. package/src/rules/rules.mts +1 -1
  139. package/src/types.mts +2 -2
@@ -5,6 +5,7 @@ import {
5
5
  } from '@typescript-eslint/utils';
6
6
  import { Arr } from 'ts-data-forge';
7
7
  import * as ts from 'typescript';
8
+ import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
8
9
  import {
9
10
  buildImportFixes,
10
11
  getImportedLocalName,
@@ -79,18 +80,29 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
79
80
  ): void => {
80
81
  const arrayExpression = getSetSourceArray(setNode);
81
82
 
82
- if (arrayExpression === undefined) return;
83
+ if (arrayExpression === undefined) {
84
+ return;
85
+ }
83
86
 
84
- if (!isGlobal(sourceCode, node, 'Set')) return;
87
+ if (!isGlobal(sourceCode, node, 'Set')) {
88
+ return;
89
+ }
85
90
 
86
- const arrayTsNode = getTsNode(arrayExpression);
91
+ // At every layer of type wrappers (see typeWrapperLayers).
92
+ const isPrimitiveArray = typeWrapperLayers(arrayExpression).every(
93
+ (layer) => {
94
+ const tsNode = getTsNode(layer);
87
95
 
88
- if (arrayTsNode === undefined) return;
96
+ return (
97
+ tsNode !== undefined &&
98
+ isPrimitiveArrayType(checker, checker.getTypeAtLocation(tsNode))
99
+ );
100
+ },
101
+ );
89
102
 
90
- if (
91
- !isPrimitiveArrayType(checker, checker.getTypeAtLocation(arrayTsNode))
92
- )
103
+ if (!isPrimitiveArray) {
93
104
  return;
105
+ }
94
106
 
95
107
  mut_matches.push({
96
108
  node,
@@ -117,17 +129,23 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
117
129
  return;
118
130
  }
119
131
 
120
- if (!isGlobal(sourceCode, node, 'Array')) return;
132
+ if (!isGlobal(sourceCode, node, 'Array')) {
133
+ return;
134
+ }
121
135
 
122
136
  check(node, node.arguments[0]);
123
137
  },
124
138
  ArrayExpression: (node) => {
125
139
  // [...new Set(xs)]
126
- if (!Arr.isFixedLengthTuple(1, node.elements)) return;
140
+ if (!Arr.isFixedLengthTuple(1, node.elements)) {
141
+ return;
142
+ }
127
143
 
128
144
  const element = node.elements[0];
129
145
 
130
- if (element?.type !== AST_NODE_TYPES.SpreadElement) return;
146
+ if (element?.type !== AST_NODE_TYPES.SpreadElement) {
147
+ return;
148
+ }
131
149
 
132
150
  check(node, element.argument);
133
151
  },
@@ -208,8 +226,11 @@ const mutatingMethods: ReadonlySet<string> = new Set([
208
226
  */
209
227
  const getSetSourceArray = (
210
228
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
211
- node: TSESTree.Node,
229
+ wrapped: TSESTree.Node,
212
230
  ): TSESTree.Expression | undefined => {
231
+ // `new Set(xs) as ReadonlySet<string>` is still `new Set(xs)`.
232
+ const node = skipTypeWrappers(wrapped);
233
+
213
234
  if (
214
235
  node.type !== AST_NODE_TYPES.NewExpression ||
215
236
  node.callee.type !== AST_NODE_TYPES.Identifier ||
@@ -246,11 +267,10 @@ const findVariable = (
246
267
  ): TSESLint.Scope.Variable | undefined => {
247
268
  const variable = scope.set.get(name);
248
269
 
249
- if (variable !== undefined) return variable;
250
-
251
- if (scope.upper === null) return undefined;
252
-
253
- return findVariable(scope.upper, name);
270
+ return (
271
+ variable ??
272
+ (scope.upper === null ? undefined : findVariable(scope.upper, name))
273
+ );
254
274
  };
255
275
 
256
276
  /**
@@ -296,11 +316,9 @@ const isPrimitiveType = (
296
316
  ): boolean => {
297
317
  const resolved = resolveConstraint(checker, type);
298
318
 
299
- if (resolved.isUnion()) {
300
- return resolved.types.every((t) => isPrimitiveType(checker, t));
301
- }
302
-
303
- return (resolved.flags & primitiveTypeFlags) !== 0;
319
+ return resolved.isUnion()
320
+ ? resolved.types.every((t) => isPrimitiveType(checker, t))
321
+ : (resolved.flags & primitiveTypeFlags) !== 0;
304
322
  };
305
323
 
306
324
  const primitiveTypeFlags =
@@ -338,13 +356,17 @@ const needsMutableResult = (
338
356
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
339
357
  node: TSESTree.Node,
340
358
  ): boolean => {
341
- if (isUsedAsMutable(checker, getTsNode, node)) return true;
359
+ const outer = outermostTypePreservingWrapper(node);
342
360
 
343
- const parent = node.parent;
361
+ if (isUsedAsMutable(checker, getTsNode, outer)) {
362
+ return true;
363
+ }
364
+
365
+ const parent = outer.parent;
344
366
 
345
367
  if (
346
368
  parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
347
- parent.init !== node ||
369
+ parent.init !== outer ||
348
370
  parent.id.type !== AST_NODE_TYPES.Identifier ||
349
371
  parent.id.typeAnnotation !== undefined
350
372
  ) {
@@ -357,11 +379,35 @@ const needsMutableResult = (
357
379
  variable?.references.some(
358
380
  (reference) =>
359
381
  reference.init !== true &&
360
- isUsedAsMutable(checker, getTsNode, reference.identifier),
382
+ isUsedAsMutable(
383
+ checker,
384
+ getTsNode,
385
+ outermostTypePreservingWrapper(reference.identifier),
386
+ ),
361
387
  ) ?? false
362
388
  );
363
389
  };
364
390
 
391
+ /**
392
+ * `node`, or the outermost `!` / `satisfies` around it. Neither changes the
393
+ * type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
394
+ * `ys!.push(4)` still need a mutable array. An `as` does change it, and a
395
+ * readonly array may be cast to a mutable one, so the walk stops there.
396
+ */
397
+ const outermostTypePreservingWrapper = (
398
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
399
+ node: TSESTree.Node,
400
+ ): TSESTree.Node => {
401
+ const { parent } = node;
402
+
403
+ return parent !== undefined &&
404
+ (parent.type === AST_NODE_TYPES.TSNonNullExpression ||
405
+ parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
406
+ parent.expression === node
407
+ ? outermostTypePreservingWrapper(parent)
408
+ : node;
409
+ };
410
+
365
411
  const isUsedAsMutable = (
366
412
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
367
413
  checker: ts.TypeChecker,
@@ -401,9 +447,13 @@ const isUsedAsMutable = (
401
447
  // whose declared type is a mutable array.
402
448
  const tsNode = getTsNode(node);
403
449
 
404
- if (tsNode === undefined) return false;
450
+ if (tsNode === undefined) {
451
+ return false;
452
+ }
405
453
 
406
- if (!ts.isExpression(tsNode)) return false;
454
+ if (!ts.isExpression(tsNode)) {
455
+ return false;
456
+ }
407
457
 
408
458
  const expectedType =
409
459
  getDeclaredParameterType(checker, tsNode) ??
@@ -436,7 +486,9 @@ const getDeclaredParameterType = (
436
486
 
437
487
  const index = call.arguments?.indexOf(argument) ?? -1;
438
488
 
439
- if (index === -1) return undefined;
489
+ if (index === -1) {
490
+ return undefined;
491
+ }
440
492
 
441
493
  const declaration = checker.getResolvedSignature(call)?.getDeclaration();
442
494
 
@@ -444,11 +496,9 @@ const getDeclaredParameterType = (
444
496
 
445
497
  // A rest parameter's declared type is the array of arguments, not the type of
446
498
  // one; the contextual type is the better answer there.
447
- if (parameter === undefined || parameter.dotDotDotToken !== undefined) {
448
- return undefined;
449
- }
450
-
451
- return checker.getTypeAtLocation(parameter);
499
+ return parameter === undefined || parameter.dotDotDotToken !== undefined
500
+ ? undefined
501
+ : checker.getTypeAtLocation(parameter);
452
502
  };
453
503
 
454
504
  /**
@@ -453,3 +453,74 @@ describe('prefer-arr-uniq', () => {
453
453
  ],
454
454
  });
455
455
  });
456
+
457
+ describe('prefer-arr-uniq through type wrappers', () => {
458
+ tester.run('prefer-arr-uniq', preferArrUniq, {
459
+ valid: [],
460
+ invalid: [
461
+ {
462
+ name: 'a Set wrapped in `as` or `satisfies`',
463
+ code: dedent`
464
+ declare const xs: readonly string[];
465
+ const a = Array.from(new Set(xs) as ReadonlySet<string>);
466
+ const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
467
+ `,
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
+ `,
474
+ errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
475
+ },
476
+ {
477
+ name: 'only suggests when the result is mutated through `!`',
478
+ code: dedent`
479
+ const xs = [3, 1, 3];
480
+ Array.from(new Set(xs))!.push(4);
481
+ `,
482
+ output: null,
483
+ errors: [
484
+ {
485
+ messageId: 'useArrUniq',
486
+ suggestions: [
487
+ {
488
+ messageId: 'suggestArrUniq',
489
+ output: dedent`
490
+ import { Arr } from 'ts-data-forge';
491
+ const xs = [3, 1, 3];
492
+ Arr.uniq(xs)!.push(4);
493
+ `,
494
+ },
495
+ ],
496
+ },
497
+ ],
498
+ },
499
+ {
500
+ name: 'only suggests when a variable initialized through `!` is mutated',
501
+ code: dedent`
502
+ const xs = [3, 1, 3];
503
+ const ys = Array.from(new Set(xs))!;
504
+ ys.push(4);
505
+ `,
506
+ output: null,
507
+ errors: [
508
+ {
509
+ messageId: 'useArrUniq',
510
+ suggestions: [
511
+ {
512
+ messageId: 'suggestArrUniq',
513
+ output: dedent`
514
+ import { Arr } from 'ts-data-forge';
515
+ const xs = [3, 1, 3];
516
+ const ys = Arr.uniq(xs)!;
517
+ ys.push(4);
518
+ `,
519
+ },
520
+ ],
521
+ },
522
+ ],
523
+ },
524
+ ],
525
+ });
526
+ });
@@ -3,7 +3,8 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { type DeepReadonly } from 'ts-type-forge';
6
+ import type { DeepReadonly } from 'ts-type-forge';
7
+ import { toArgumentText } from './ast-utils.mjs';
7
8
  import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
8
9
  import {
9
10
  buildImportFixes,
@@ -38,47 +39,43 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
38
39
  const tsDataForgeImport = getTsDataForgeImport(program);
39
40
 
40
41
  const mut_nodesToFix: {
41
- node: TSESTree.TSAsExpression;
42
+ node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
42
43
  typeName: string;
43
44
  functionName: string;
44
45
  }[] = [];
45
46
 
46
- return {
47
- TSAsExpression: (node) => {
48
- const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
49
-
50
- if (typeInfo === undefined) return;
47
+ // `x as Int` and `<Int>x` are the same assertion.
48
+ const collect = (
49
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
50
+ node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
51
+ ): void => {
52
+ const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
53
+
54
+ if (typeInfo === undefined) {
55
+ return;
56
+ }
57
+
58
+ mut_nodesToFix.push({
59
+ node,
60
+ typeName: typeInfo.typeName,
61
+ functionName: typeInfo.functionName,
62
+ });
63
+ };
51
64
 
52
- mut_nodesToFix.push({
53
- node,
54
- typeName: typeInfo.typeName,
55
- functionName: typeInfo.functionName,
56
- });
57
- },
65
+ return {
66
+ TSAsExpression: collect,
67
+ TSTypeAssertion: collect,
58
68
  'Program:exit': () => {
59
69
  const namedImports = getNamedImports(tsDataForgeImport);
60
70
 
61
71
  // Group nodes by function name to handle imports efficiently
62
- const mut_functionNameToNodes = new Map<
63
- string,
64
- Readonly<{
65
- node: TSESTree.TSAsExpression;
66
- typeName: string;
67
- functionName: string;
68
- }>[]
69
- >();
70
-
71
- for (const nodeInfo of mut_nodesToFix) {
72
- const mut_nodes =
73
- mut_functionNameToNodes.get(nodeInfo.functionName) ?? [];
74
-
75
- mut_nodes.push(nodeInfo);
76
-
77
- mut_functionNameToNodes.set(nodeInfo.functionName, mut_nodes);
78
- }
72
+ const functionNameToNodes = Map.groupBy(
73
+ mut_nodesToFix,
74
+ (nodeInfo) => nodeInfo.functionName,
75
+ );
79
76
 
80
77
  // Process each group
81
- for (const [functionName, nodes] of mut_functionNameToNodes) {
78
+ for (const [functionName, nodes] of functionNameToNodes) {
82
79
  const hasImport = namedImports.includes(functionName);
83
80
 
84
81
  for (const [index, { node, typeName }] of nodes.entries()) {
@@ -90,7 +87,7 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
90
87
  functionName,
91
88
  },
92
89
  fix: (fixer) => {
93
- const replacement = `${functionName}(${sourceCode.getText(node.expression)})`;
90
+ const replacement = `${functionName}(${toArgumentText(node.expression, sourceCode)})`;
94
91
 
95
92
  // Add import only for the first node of this function and only if not already imported
96
93
  const importFixes =
@@ -114,18 +111,21 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
114
111
  const getBrandedNumberTypeInfo = (
115
112
  typeAnnotation: DeepReadonly<TSESTree.TypeNode>,
116
113
  ): Readonly<{ typeName: string; functionName: string }> | undefined => {
117
- if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) return undefined;
114
+ if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) {
115
+ return undefined;
116
+ }
118
117
 
119
- if (typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier)
118
+ if (typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier) {
120
119
  return undefined;
120
+ }
121
121
 
122
122
  const typeName = typeAnnotation.typeName.name;
123
123
 
124
- if (!brandedNumberTypeNameToFunctionName.has(typeName)) return undefined;
124
+ if (!brandedNumberTypeNameToFunctionName.has(typeName)) {
125
+ return undefined;
126
+ }
125
127
 
126
128
  const functionName = brandedNumberTypeNameToFunctionName.get(typeName);
127
129
 
128
- if (functionName === undefined) return undefined;
129
-
130
- return { typeName, functionName };
130
+ return functionName === undefined ? undefined : { typeName, functionName };
131
131
  };
@@ -265,3 +265,54 @@ describe('prefer-as-int', () => {
265
265
  ],
266
266
  });
267
267
  }, 20000);
268
+
269
+ describe('prefer-as-int through type wrappers', () => {
270
+ tester.run('prefer-as-int', preferAsInt, {
271
+ valid: [],
272
+ invalid: [
273
+ {
274
+ name: 'an angle-bracket assertion',
275
+ code: dedent`
276
+ import { asInt } from 'ts-data-forge';
277
+ declare const n: number;
278
+ const a = <Int>n;
279
+ `,
280
+ output: dedent`
281
+ import { asInt } from 'ts-data-forge';
282
+ declare const n: number;
283
+ const a = asInt(n);
284
+ `,
285
+ errors: [{ messageId: 'useBrandedNumberCastFunction' }],
286
+ },
287
+ ],
288
+ });
289
+ }, 20000);
290
+
291
+ describe('prefer-as-int with parenthesized operands', () => {
292
+ tester.run('prefer-as-int', preferAsInt, {
293
+ valid: [],
294
+ invalid: [
295
+ {
296
+ name: 'a sequence operand keeps its parentheses',
297
+ code: dedent`
298
+ import { asInt } from 'ts-data-forge';
299
+ declare const log: () => void;
300
+ declare const n: number;
301
+ const a = (log(), n) as Int;
302
+ const b = <Int>(log(), n);
303
+ `,
304
+ output: dedent`
305
+ import { asInt } from 'ts-data-forge';
306
+ declare const log: () => void;
307
+ declare const n: number;
308
+ const a = asInt((log(), n));
309
+ const b = asInt((log(), n));
310
+ `,
311
+ errors: [
312
+ { messageId: 'useBrandedNumberCastFunction' },
313
+ { messageId: 'useBrandedNumberCastFunction' },
314
+ ],
315
+ },
316
+ ],
317
+ });
318
+ }, 20000);