eslint-plugin-ts-data-forge 0.8.5 → 0.9.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 (135) hide show
  1. package/README.md +20 -19
  2. package/dist/plugin.d.mts +2 -0
  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 +16 -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 -34
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +53 -58
  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/no-unnecessary-type-guard.d.mts.map +1 -1
  22. package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
  23. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  25. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  26. package/dist/rules/prefer-arr-generate.mjs +588 -0
  27. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  28. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  29. package/dist/rules/prefer-arr-is-array.mjs +7 -5
  30. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  31. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  32. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +9 -6
  33. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  34. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  35. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +9 -6
  36. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  37. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -6
  39. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -6
  42. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  44. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -6
  45. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  46. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  47. package/dist/rules/prefer-arr-scan.mjs +8 -11
  48. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  49. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  50. package/dist/rules/prefer-arr-sum.mjs +8 -6
  51. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  52. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  53. package/dist/rules/prefer-arr-uniq.mjs +12 -166
  54. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  55. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  56. package/dist/rules/prefer-as-int.mjs +6 -8
  57. package/dist/rules/prefer-as-int.mjs.map +1 -1
  58. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  59. package/dist/rules/prefer-canonical-array-slicing.mjs +57 -34
  60. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  61. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  62. package/dist/rules/prefer-canonical-length-cast.mjs +3 -3
  63. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  64. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  65. package/dist/rules/prefer-canonical-length-guard.mjs +4 -4
  66. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  67. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  68. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +3 -2
  69. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  70. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  71. package/dist/rules/prefer-is-non-null-object.mjs +113 -43
  72. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  73. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  74. package/dist/rules/prefer-is-record-and-has-key.mjs +105 -48
  75. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  77. package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
  78. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  80. package/dist/rules/prefer-num-safe-parse-int.mjs +6 -6
  81. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +9 -12
  84. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  86. package/dist/rules/prefer-range-for-loop.mjs +104 -23
  87. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  88. package/dist/rules/rules.d.mts +1 -0
  89. package/dist/rules/rules.d.mts.map +1 -1
  90. package/dist/rules/rules.mjs +2 -0
  91. package/dist/rules/rules.mjs.map +1 -1
  92. package/package.json +11 -10
  93. package/src/plugin.mts +1 -0
  94. package/src/rule-types.mts +17 -1
  95. package/src/rules/ast-utils.mts +73 -80
  96. package/src/rules/constants.mts +12 -0
  97. package/src/rules/mutable-result.mts +204 -0
  98. package/src/rules/no-unnecessary-type-guard.mts +14 -10
  99. package/src/rules/prefer-arr-generate.mts +918 -0
  100. package/src/rules/prefer-arr-generate.test.mts +967 -0
  101. package/src/rules/prefer-arr-is-array.mts +14 -7
  102. package/src/rules/prefer-arr-is-array.test.mts +14 -0
  103. package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
  104. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  105. package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
  106. package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
  107. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  108. package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
  109. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  110. package/src/rules/prefer-arr-is-non-empty.mts +16 -13
  111. package/src/rules/prefer-arr-scan.mts +24 -19
  112. package/src/rules/prefer-arr-sum.mts +18 -14
  113. package/src/rules/prefer-arr-uniq.mts +25 -234
  114. package/src/rules/prefer-as-int.mts +19 -12
  115. package/src/rules/prefer-canonical-array-slicing.mts +79 -52
  116. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  117. package/src/rules/prefer-canonical-length-cast.mts +7 -3
  118. package/src/rules/prefer-canonical-length-guard.mts +9 -4
  119. package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
  120. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
  121. package/src/rules/prefer-is-non-null-object.mts +171 -64
  122. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  123. package/src/rules/prefer-is-record-and-has-key.mts +159 -74
  124. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  125. package/src/rules/prefer-num-safe-parse-float.mts +15 -10
  126. package/src/rules/prefer-num-safe-parse-int.mts +15 -13
  127. package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
  128. package/src/rules/prefer-range-for-loop.mts +157 -31
  129. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  130. package/src/rules/rules.mts +2 -0
  131. package/dist/rules/import-utils.d.mts +0 -30
  132. package/dist/rules/import-utils.d.mts.map +0 -1
  133. package/dist/rules/import-utils.mjs +0 -98
  134. package/dist/rules/import-utils.mjs.map +0 -1
  135. package/src/rules/import-utils.mts +0 -175
@@ -1,16 +1,21 @@
1
1
  import {
2
2
  AST_NODE_TYPES,
3
+ ASTUtils,
3
4
  type TSESLint,
4
5
  type TSESTree,
5
6
  } from '@typescript-eslint/utils';
6
- import { Arr } from 'ts-data-forge';
7
- import * as ts from 'typescript';
8
- import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
9
7
  import {
10
- buildImportFixes,
8
+ buildNamedImportFix,
9
+ getArgumentText,
10
+ getImportDeclarations,
11
11
  getImportedLocalName,
12
- getTsDataForgeImport,
13
- } from './import-utils.mjs';
12
+ skipTypeWrappers,
13
+ typeWrapperLayers,
14
+ } from 'eslint-rule-forge';
15
+ import { Arr } from 'ts-data-forge';
16
+ import * as ts from 'typescript';
17
+ import { primitiveTypeFlags, TS_DATA_FORGE_MODULE } from './constants.mjs';
18
+ import { needsMutableResult } from './mutable-result.mjs';
14
19
 
15
20
  type Options = readonly [];
16
21
 
@@ -47,7 +52,10 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
47
52
 
48
53
  const program = sourceCode.ast;
49
54
 
50
- const tsDataForgeImport = getTsDataForgeImport(program);
55
+ const tsDataForgeImports = getImportDeclarations(
56
+ program,
57
+ TS_DATA_FORGE_MODULE,
58
+ );
51
59
 
52
60
  const services = sourceCode.parserServices;
53
61
 
@@ -150,7 +158,7 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
150
158
  check(node, element.argument);
151
159
  },
152
160
  'Program:exit': () => {
153
- const arrLocalName = getImportedLocalName(tsDataForgeImport, 'Arr');
161
+ const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
154
162
 
155
163
  const arrName = arrLocalName ?? 'Arr';
156
164
 
@@ -162,16 +170,18 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
162
170
  for (const match of mut_matches) {
163
171
  const arrayText = sourceCode.getText(match.arrayExpression);
164
172
 
165
- const argumentText =
166
- match.arrayExpression.type === AST_NODE_TYPES.SequenceExpression
167
- ? `(${arrayText})`
168
- : arrayText;
173
+ const argumentText = getArgumentText(
174
+ sourceCode,
175
+ match.arrayExpression,
176
+ );
169
177
 
170
178
  const buildFix =
171
179
  (withImport: boolean): TSESLint.ReportFixFunction =>
172
180
  (fixer) => [
173
181
  ...(withImport && arrLocalName === undefined
174
- ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
182
+ ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
183
+ 'Arr',
184
+ ])
175
185
  : []),
176
186
  fixer.replaceText(match.node, `${arrName}.uniq(${argumentText})`),
177
187
  ];
@@ -206,19 +216,6 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
206
216
  defaultOptions: [],
207
217
  } as const;
208
218
 
209
- /** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
210
- const mutatingMethods: ReadonlySet<string> = new Set([
211
- 'copyWithin',
212
- 'fill',
213
- 'pop',
214
- 'push',
215
- 'reverse',
216
- 'shift',
217
- 'sort',
218
- 'splice',
219
- 'unshift',
220
- ]);
221
-
222
219
  /**
223
220
  * Returns `xs` for `new Set(xs)`, and `undefined` for anything else — including
224
221
  * `new Set()`, `new Set<T>(xs)` (whose element type may be wider than `xs`'s)
@@ -254,23 +251,10 @@ const isGlobal = (
254
251
  node: TSESTree.Node,
255
252
  name: string,
256
253
  ): boolean => {
257
- const variable = findVariable(sourceCode.getScope(node), name);
254
+ const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);
258
255
 
259
256
  // Globals from the TypeScript lib are implicit variables with no definition.
260
- return variable === undefined || variable.defs.length === 0;
261
- };
262
-
263
- const findVariable = (
264
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
265
- scope: TSESLint.Scope.Scope,
266
- name: string,
267
- ): TSESLint.Scope.Variable | undefined => {
268
- const variable = scope.set.get(name);
269
-
270
- return (
271
- variable ??
272
- (scope.upper === null ? undefined : findVariable(scope.upper, name))
273
- );
257
+ return variable === null || variable.defs.length === 0;
274
258
  };
275
259
 
276
260
  /**
@@ -321,15 +305,6 @@ const isPrimitiveType = (
321
305
  : (resolved.flags & primitiveTypeFlags) !== 0;
322
306
  };
323
307
 
324
- const primitiveTypeFlags =
325
- ts.TypeFlags.StringLike |
326
- ts.TypeFlags.NumberLike |
327
- ts.TypeFlags.BigIntLike |
328
- ts.TypeFlags.BooleanLike |
329
- ts.TypeFlags.ESSymbolLike |
330
- ts.TypeFlags.Null |
331
- ts.TypeFlags.Undefined;
332
-
333
308
  const resolveConstraint = (
334
309
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
335
310
  checker: ts.TypeChecker,
@@ -339,187 +314,3 @@ const resolveConstraint = (
339
314
  (type.flags & ts.TypeFlags.TypeParameter) !== 0
340
315
  ? (checker.getBaseConstraintOfType(type) ?? type)
341
316
  : type;
342
-
343
- /**
344
- * Whether the code around `node` visibly relies on the result being a mutable
345
- * array, so that swapping in the readonly array `Arr.uniq` returns would not
346
- * compile. Looks at `node` itself and, when it initializes a variable without a
347
- * type annotation, at every read of that variable.
348
- */
349
- const needsMutableResult = (
350
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
351
- sourceCode: TSESLint.SourceCode,
352
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
353
- checker: ts.TypeChecker,
354
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
355
- getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
356
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
357
- node: TSESTree.Node,
358
- ): boolean => {
359
- const outer = outermostTypePreservingWrapper(node);
360
-
361
- if (isUsedAsMutable(checker, getTsNode, outer)) {
362
- return true;
363
- }
364
-
365
- const parent = outer.parent;
366
-
367
- if (
368
- parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
369
- parent.init !== outer ||
370
- parent.id.type !== AST_NODE_TYPES.Identifier ||
371
- parent.id.typeAnnotation !== undefined
372
- ) {
373
- return false;
374
- }
375
-
376
- const [variable] = sourceCode.getDeclaredVariables(parent);
377
-
378
- return (
379
- variable?.references.some(
380
- (reference) =>
381
- reference.init !== true &&
382
- isUsedAsMutable(
383
- checker,
384
- getTsNode,
385
- outermostTypePreservingWrapper(reference.identifier),
386
- ),
387
- ) ?? false
388
- );
389
- };
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
-
411
- const isUsedAsMutable = (
412
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
413
- checker: ts.TypeChecker,
414
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
415
- getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
416
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
417
- node: TSESTree.Node,
418
- ): boolean => {
419
- const parent = node.parent;
420
-
421
- if (
422
- parent?.type === AST_NODE_TYPES.MemberExpression &&
423
- parent.object === node
424
- ) {
425
- // xs.push(...), xs.sort(), ...
426
- if (
427
- !parent.computed &&
428
- parent.property.type === AST_NODE_TYPES.Identifier &&
429
- mutatingMethods.has(parent.property.name)
430
- ) {
431
- return true;
432
- }
433
-
434
- // xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
435
- const grandParent = parent.parent;
436
-
437
- return (
438
- (grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
439
- grandParent.left === parent) ||
440
- grandParent.type === AST_NODE_TYPES.UpdateExpression ||
441
- (grandParent.type === AST_NODE_TYPES.UnaryExpression &&
442
- grandParent.operator === 'delete')
443
- );
444
- }
445
-
446
- // Passed to a parameter, assigned to a variable, or returned from a function
447
- // whose declared type is a mutable array.
448
- const tsNode = getTsNode(node);
449
-
450
- if (tsNode === undefined) {
451
- return false;
452
- }
453
-
454
- if (!ts.isExpression(tsNode)) {
455
- return false;
456
- }
457
-
458
- const expectedType =
459
- getDeclaredParameterType(checker, tsNode) ??
460
- checker.getContextualType(tsNode);
461
-
462
- return (
463
- expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
464
- );
465
- };
466
-
467
- /**
468
- * The declared type of the parameter `argument` is passed to. The contextual
469
- * type of an argument to a generic function is instantiated from that very
470
- * argument — `Arr.toSorted(Array.from(set), cmp)` has `Ar` inferred as
471
- * `string[]` — so it would read as "a mutable array is required" for any
472
- * generic parameter. The declared type (`Ar extends readonly unknown[]`) is
473
- * what the rewritten argument actually has to satisfy.
474
- */
475
- const getDeclaredParameterType = (
476
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
477
- checker: ts.TypeChecker,
478
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
479
- argument: ts.Expression,
480
- ): ts.Type | undefined => {
481
- const call = argument.parent;
482
-
483
- if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) {
484
- return undefined;
485
- }
486
-
487
- const index = call.arguments?.indexOf(argument) ?? -1;
488
-
489
- if (index === -1) {
490
- return undefined;
491
- }
492
-
493
- const declaration = checker.getResolvedSignature(call)?.getDeclaration();
494
-
495
- const parameter = declaration?.parameters[index];
496
-
497
- // A rest parameter's declared type is the array of arguments, not the type of
498
- // one; the contextual type is the better answer there.
499
- return parameter === undefined || parameter.dotDotDotToken !== undefined
500
- ? undefined
501
- : checker.getTypeAtLocation(parameter);
502
- };
503
-
504
- /**
505
- * Whether a readonly array cannot be assigned to `type`: it is a mutable array,
506
- * or a union of mutable arrays and primitives. (A tuple accepted no array to
507
- * begin with, so it does not matter here.)
508
- */
509
- const isMutableArrayOnly = (
510
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
511
- checker: ts.TypeChecker,
512
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
513
- type: ts.Type,
514
- ): boolean => {
515
- const members = type.isUnion() ? type.types : ([type] as const);
516
-
517
- return (
518
- members.every(
519
- (t) =>
520
- // `null`, `undefined` and other primitives accept no array at all.
521
- (t.flags & primitiveTypeFlags) !== 0 ||
522
- (checker.isArrayType(t) && t.getSymbol()?.getName() === 'Array'),
523
- ) && members.some((t) => (t.flags & primitiveTypeFlags) === 0)
524
- );
525
- };
@@ -3,14 +3,15 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import type { DeepReadonly } from 'ts-type-forge';
7
- import { toArgumentText } from './ast-utils.mjs';
8
- import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
9
6
  import {
10
- buildImportFixes,
7
+ buildNamedImportFix,
8
+ getArgumentText,
9
+ getImportDeclarations,
11
10
  getNamedImports,
12
- getTsDataForgeImport,
13
- } from './import-utils.mjs';
11
+ } from 'eslint-rule-forge';
12
+ import type { DeepReadonly } from 'ts-type-forge';
13
+ import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
14
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
14
15
 
15
16
  type Options = readonly [];
16
17
 
@@ -36,7 +37,10 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
36
37
 
37
38
  const program = sourceCode.ast;
38
39
 
39
- const tsDataForgeImport = getTsDataForgeImport(program);
40
+ const tsDataForgeImports = getImportDeclarations(
41
+ program,
42
+ TS_DATA_FORGE_MODULE,
43
+ );
40
44
 
41
45
  const mut_nodesToFix: {
42
46
  node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
@@ -66,7 +70,7 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
66
70
  TSAsExpression: collect,
67
71
  TSTypeAssertion: collect,
68
72
  'Program:exit': () => {
69
- const namedImports = getNamedImports(tsDataForgeImport);
73
+ const namedImports = getNamedImports(tsDataForgeImports);
70
74
 
71
75
  // Group nodes by function name to handle imports efficiently
72
76
  const functionNameToNodes = Map.groupBy(
@@ -87,14 +91,17 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
87
91
  functionName,
88
92
  },
89
93
  fix: (fixer) => {
90
- const replacement = `${functionName}(${toArgumentText(node.expression, sourceCode)})`;
94
+ const replacement = `${functionName}(${getArgumentText(sourceCode, node.expression)})`;
91
95
 
92
96
  // Add import only for the first node of this function and only if not already imported
93
97
  const importFixes =
94
98
  index === 0 && !hasImport
95
- ? buildImportFixes(fixer, program, tsDataForgeImport, [
96
- functionName,
97
- ])
99
+ ? buildNamedImportFix(
100
+ fixer,
101
+ program,
102
+ TS_DATA_FORGE_MODULE,
103
+ [functionName],
104
+ )
98
105
  : [];
99
106
 
100
107
  return [...importFixes, fixer.replaceText(node, replacement)];
@@ -4,14 +4,16 @@ import {
4
4
  type TSESLint,
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
- import type { ReadonlyRecord } from 'ts-type-forge';
8
- import * as ts from 'typescript';
9
- import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
10
7
  import {
11
- buildImportFixes,
8
+ buildNamedImportFix,
9
+ getImportDeclarations,
12
10
  getImportedLocalName,
13
- getTsDataForgeImport,
14
- } from './import-utils.mjs';
11
+ skipTypeWrappers,
12
+ typeWrapperLayers,
13
+ } from 'eslint-rule-forge';
14
+ import type { ReadonlyRecord } from 'ts-type-forge';
15
+ import * as ts from 'typescript';
16
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
15
17
 
16
18
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
17
19
 
@@ -82,23 +84,32 @@ const asNegatedPositiveIntegerLiteral = (
82
84
 
83
85
  /**
84
86
  * Whether re-reading the expression is free of side effects (a plain variable
85
- * or a non-computed property chain). Required for patterns that reference the
86
- * receiver twice, such as `xs.filter((_, i) => i < xs.length - 1)`.
87
+ * or a non-computed property chain, type wrappers aside). Required for
88
+ * patterns that reference the receiver twice, such as
89
+ * `xs.filter((_, i) => i < xs.length - 1)`.
87
90
  */
88
- const isSimpleReference = (node: TSESTree.Node): boolean =>
89
- node.type === AST_NODE_TYPES.Identifier ||
90
- node.type === AST_NODE_TYPES.ThisExpression ||
91
- (node.type === AST_NODE_TYPES.MemberExpression &&
92
- !node.computed &&
93
- node.property.type === AST_NODE_TYPES.Identifier &&
94
- isSimpleReference(node.object));
95
-
96
- const rootIdentifierName = (node: TSESTree.Node): string | undefined =>
97
- node.type === AST_NODE_TYPES.Identifier
91
+ const isSimpleReference = (wrapped: TSESTree.Node): boolean => {
92
+ const node = skipTypeWrappers(wrapped);
93
+
94
+ return (
95
+ node.type === AST_NODE_TYPES.Identifier ||
96
+ node.type === AST_NODE_TYPES.ThisExpression ||
97
+ (node.type === AST_NODE_TYPES.MemberExpression &&
98
+ !node.computed &&
99
+ node.property.type === AST_NODE_TYPES.Identifier &&
100
+ isSimpleReference(node.object))
101
+ );
102
+ };
103
+
104
+ const rootIdentifierName = (wrapped: TSESTree.Node): string | undefined => {
105
+ const node = skipTypeWrappers(wrapped);
106
+
107
+ return node.type === AST_NODE_TYPES.Identifier
98
108
  ? node.name
99
109
  : node.type === AST_NODE_TYPES.MemberExpression
100
110
  ? rootIdentifierName(node.object)
101
111
  : undefined;
112
+ };
102
113
 
103
114
  type ComparisonOperator = '!==' | '<' | '<=' | '>' | '>=';
104
115
 
@@ -188,7 +199,10 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
188
199
 
189
200
  const program = sourceCode.ast;
190
201
 
191
- const tsDataForgeImport = getTsDataForgeImport(program);
202
+ const tsDataForgeImports = getImportDeclarations(
203
+ program,
204
+ TS_DATA_FORGE_MODULE,
205
+ );
192
206
 
193
207
  const parserServices = ESLintUtils.getParserServices(context);
194
208
 
@@ -199,11 +213,14 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
199
213
  * (possibly aliased), a namespace import member, or the canonical `Arr`
200
214
  * (added by the import fix when nothing is imported yet).
201
215
  */
202
- const arrLocalName = getImportedLocalName(tsDataForgeImport, 'Arr');
216
+ const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
203
217
 
204
- const namespaceLocalName = tsDataForgeImport?.specifiers.find(
205
- (specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
206
- )?.local.name;
218
+ const namespaceLocalName = tsDataForgeImports
219
+ .flatMap((declaration) => declaration.specifiers)
220
+ .find(
221
+ (specifier) =>
222
+ specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
223
+ )?.local.name;
207
224
 
208
225
  const arrRef =
209
226
  arrLocalName ??
@@ -330,26 +347,41 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
330
347
  });
331
348
  };
332
349
 
333
- /** Matches `<xsText>.length` (non-computed). */
350
+ /**
351
+ * Matches `<receiver>.length` (non-computed), type wrappers aside on the
352
+ * access, on its object and on the receiver:
353
+ * `(xs as readonly T[]).length` reads the length of `xs`.
354
+ */
334
355
  const isReceiverLengthAccess = (
335
- node: TSESTree.Node,
336
- receiverText: string,
337
- ): boolean =>
338
- node.type === AST_NODE_TYPES.MemberExpression &&
339
- !node.computed &&
340
- node.property.type === AST_NODE_TYPES.Identifier &&
341
- node.property.name === 'length' &&
342
- sourceCode.getText(node.object) === receiverText;
356
+ wrapped: TSESTree.Node,
357
+ receiver: TSESTree.Expression,
358
+ ): boolean => {
359
+ const node = skipTypeWrappers(wrapped);
360
+
361
+ return (
362
+ node.type === AST_NODE_TYPES.MemberExpression &&
363
+ !node.computed &&
364
+ node.property.type === AST_NODE_TYPES.Identifier &&
365
+ node.property.name === 'length' &&
366
+ sourceCode.getText(skipTypeWrappers(node.object)) ===
367
+ sourceCode.getText(skipTypeWrappers(receiver))
368
+ );
369
+ };
343
370
 
344
- /** Matches `<xsText>.length - M` (M ≥ 1) and returns `M`. */
371
+ /**
372
+ * Matches `<receiver>.length - M` (M ≥ 1), under any type wrapper, and
373
+ * returns `M`.
374
+ */
345
375
  const matchLengthMinus = (
346
- node: TSESTree.Node,
347
- receiverText: string,
376
+ wrapped: TSESTree.Node,
377
+ receiver: TSESTree.Expression,
348
378
  ): number | undefined => {
379
+ const node = skipTypeWrappers(wrapped);
380
+
349
381
  if (
350
382
  node.type !== AST_NODE_TYPES.BinaryExpression ||
351
383
  node.operator !== '-' ||
352
- !isReceiverLengthAccess(node.left, receiverText)
384
+ !isReceiverLengthAccess(node.left, receiver)
353
385
  ) {
354
386
  return undefined;
355
387
  }
@@ -367,7 +399,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
367
399
  const classifyFilterCallback = (
368
400
  callback: TSESTree.CallExpressionArgument,
369
401
  receiver: TSESTree.Expression,
370
- receiverText: string,
371
402
  ): CopyOp | undefined => {
372
403
  if (
373
404
  callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
@@ -401,7 +432,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
401
432
  return undefined;
402
433
  }
403
434
 
404
- const body = callback.body;
435
+ // `(i >= 1) satisfies boolean` is still the comparison.
436
+ const body = skipTypeWrappers(callback.body);
405
437
 
406
438
  if (
407
439
  body.type !== AST_NODE_TYPES.BinaryExpression ||
@@ -448,7 +480,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
448
480
  return undefined;
449
481
  }
450
482
 
451
- const m = matchLengthMinus(other, receiverText);
483
+ const m = matchLengthMinus(other, receiver);
452
484
 
453
485
  return m === undefined ? undefined : classifyLengthComparison(op, m);
454
486
  };
@@ -504,7 +536,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
504
536
  return undefined;
505
537
  }
506
538
 
507
- const m = matchLengthMinus(end, sourceCode.getText(receiver));
539
+ const m = matchLengthMinus(end, receiver);
508
540
 
509
541
  return m === undefined ? undefined : normalizeCountedOp('skipLast', m);
510
542
  }
@@ -515,7 +547,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
515
547
  const classifyToSplicedArgs = (
516
548
  args: readonly TSESTree.CallExpressionArgument[],
517
549
  receiver: TSESTree.Expression,
518
- receiverText: string,
519
550
  ): CopyOp | undefined => {
520
551
  if (args.length === 1) {
521
552
  const [start] = args;
@@ -564,7 +595,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
564
595
  return undefined;
565
596
  }
566
597
 
567
- const m = matchLengthMinus(deleteCount, receiverText);
598
+ const m = matchLengthMinus(deleteCount, receiver);
568
599
 
569
600
  return m === undefined ? undefined : normalizeCountedOp('takeLast', m);
570
601
  }
@@ -590,7 +621,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
590
621
  // `xs.toSpliced(xs.length, 0, item)` appends → toPushed.
591
622
  if (
592
623
  isSimpleReference(receiver) &&
593
- isReceiverLengthAccess(start, receiverText)
624
+ isReceiverLengthAccess(start, receiver)
594
625
  ) {
595
626
  return { fn: 'toPushed', itemText: toArgText(item) };
596
627
  }
@@ -694,8 +725,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
694
725
  return;
695
726
  }
696
727
 
697
- const receiverText = sourceCode.getText(receiver);
698
-
699
728
  switch (methodName) {
700
729
  case 'slice': {
701
730
  const op = classifySliceArgs(node.arguments, receiver);
@@ -708,11 +737,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
708
737
  }
709
738
 
710
739
  case 'toSpliced': {
711
- const op = classifyToSplicedArgs(
712
- node.arguments,
713
- receiver,
714
- receiverText,
715
- );
740
+ const op = classifyToSplicedArgs(node.arguments, receiver);
716
741
 
717
742
  if (op !== undefined) {
718
743
  report(node, op, toArgText(receiver));
@@ -728,7 +753,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
728
753
  return;
729
754
  }
730
755
 
731
- const op = classifyFilterCallback(callback, receiver, receiverText);
756
+ const op = classifyFilterCallback(callback, receiver);
732
757
 
733
758
  if (op !== undefined) {
734
759
  report(node, op, toArgText(receiver));
@@ -837,7 +862,9 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
837
862
  fix: (fixer) => {
838
863
  const importFixes =
839
864
  index === 0 && needsArrImport
840
- ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
865
+ ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
866
+ 'Arr',
867
+ ])
841
868
  : [];
842
869
 
843
870
  return [