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
@@ -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);
@@ -5,11 +5,14 @@ import {
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
7
  import {
8
- buildNamedImportFix,
8
+ bindValueImports,
9
9
  getImportDeclarations,
10
10
  getImportedLocalName,
11
+ hasForeignBinding,
12
+ importedNameOf,
11
13
  skipTypeWrappers,
12
14
  typeWrapperLayers,
15
+ type ValueImportBinding,
13
16
  } from 'eslint-rule-forge';
14
17
  import type { ReadonlyRecord } from 'ts-type-forge';
15
18
  import * as ts from 'typescript';
@@ -84,8 +87,8 @@ const asNegatedPositiveIntegerLiteral = (
84
87
 
85
88
  /**
86
89
  * Whether re-reading the expression is free of side effects (a plain variable
87
- * or a non-computed property chain, type wrappers aside). Required for
88
- * patterns that reference the receiver twice, such as
90
+ * or a non-computed property chain, type wrappers aside). Required for patterns
91
+ * that reference the receiver twice, such as
89
92
  * `xs.filter((_, i) => i < xs.length - 1)`.
90
93
  */
91
94
  const isSimpleReference = (wrapped: TSESTree.Node): boolean => {
@@ -210,10 +213,16 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
210
213
  const checker = parserServices.program.getTypeChecker();
211
214
 
212
215
  /**
213
- * The in-scope way to reference the `Arr` namespace: a named import
214
- * (possibly aliased), a namespace import member, or the canonical `Arr`
215
- * (added by the import fix when nothing is imported yet).
216
+ * The in-scope way to reference the `Arr` namespace, the name the reference
217
+ * starts with, and the edits that bring it into scope: a value
218
+ * import of `Arr` (possibly aliased), a namespace import member, or `Arr`
219
+ * bound by {@link bindValueImports}, which adds it to an existing import or
220
+ * drops `type` from one. `undefined` when none can: `import type { Arr }`
221
+ * with no namespace import.
216
222
  */
223
+ const arrTarget = resolveArrTarget(sourceCode, tsDataForgeImports);
224
+
225
+ /** How a call already in the file names `Arr`, to recognize it. */
217
226
  const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
218
227
 
219
228
  const namespaceLocalName = tsDataForgeImports
@@ -223,16 +232,11 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
223
232
  specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
224
233
  )?.local.name;
225
234
 
226
- const arrRef =
227
- arrLocalName ??
228
- (namespaceLocalName === undefined ? 'Arr' : `${namespaceLocalName}.Arr`);
229
-
230
- const needsArrImport =
231
- arrLocalName === undefined && namespaceLocalName === undefined;
235
+ const arrRef = arrTarget?.ref ?? 'Arr';
232
236
 
233
237
  /**
234
- * Deferred (non-concrete) types are reduced to their base constraint
235
- * before checking; `any` / `unknown` are conservatively not array-like.
238
+ * Deferred (non-concrete) types are reduced to their base constraint before
239
+ * checking; `any` / `unknown` are conservatively not array-like.
236
240
  */
237
241
  const isArrayOrTupleType = (type: ts.Type): boolean => {
238
242
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
@@ -394,8 +398,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
394
398
 
395
399
  /**
396
400
  * Matches an index-only filter callback (`(_, i) => <comparison>`) and
397
- * classifies the comparison. Both a leading index (`i >= 1`) and a
398
- * trailing index (`1 <= i`) operand order are accepted.
401
+ * classifies the comparison. Both a leading index (`i >= 1`) and a trailing
402
+ * index (`1 <= i`) operand order are accepted.
399
403
  */
400
404
  const classifyFilterCallback = (
401
405
  callback: TSESTree.CallExpressionArgument,
@@ -467,10 +471,10 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
467
471
  return undefined;
468
472
  }
469
473
 
470
- // The element parameter must not participate in the comparison (only
471
- // the index side and a receiver-independent bound are recognized, so
472
- // reaching here with an element reference is impossible for literals,
473
- // but `xs.length - N` needs a stable receiver).
474
+ // The element parameter must not participate in the comparison (only the
475
+ // index side and a receiver-independent bound are recognized, so reaching
476
+ // here with an element reference is impossible for literals, but
477
+ // `xs.length - N` needs a stable receiver).
474
478
  const n = asNonNegativeIntegerLiteral(other);
475
479
 
476
480
  if (n !== undefined) {
@@ -720,8 +724,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
720
724
  return;
721
725
  }
722
726
 
723
- // The receiver must be an array or tuple (not a string / typed array
724
- // / other object that happens to share the method name).
727
+ // The receiver must be an array or tuple (not a string / typed array /
728
+ // other object that happens to share the method name).
725
729
  if (!holdsThroughWrappers(receiver, isArrayOrTupleType)) {
726
730
  return;
727
731
  }
@@ -794,8 +798,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
794
798
  return;
795
799
  }
796
800
 
797
- // `xs.concat(item)` appends only when `item` can never be an
798
- // array (otherwise `concat` would flatten it).
801
+ // `xs.concat(item)` appends only when `item` can never be an array
802
+ // (otherwise `concat` would flatten it).
799
803
  if (holdsThroughWrappers(arg, isDefinitelyNotArrayType)) {
800
804
  report(
801
805
  node,
@@ -835,8 +839,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
835
839
  : [undefined, undefined, undefined];
836
840
 
837
841
  // `spread`, `item`, and `fn` are set together (all from the same
838
- // tuple), so checking `spread` also guarantees the others are
839
- // defined.
842
+ // tuple), so checking `spread` also guarantees the others are defined.
840
843
  if (spread === undefined) {
841
844
  return;
842
845
  }
@@ -855,27 +858,85 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
855
858
  },
856
859
 
857
860
  'Program:exit': () => {
858
- for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
861
+ // Every fix brings `Arr` into scope itself, so that one applied alone
862
+ // (an editor's quick fix) is complete.
863
+ for (const nodeInfo of mut_nodesToFix) {
864
+ const writable =
865
+ arrTarget !== undefined &&
866
+ !hasForeignBinding(
867
+ sourceCode,
868
+ nodeInfo.node,
869
+ arrTarget.boundName,
870
+ TS_DATA_FORGE_MODULE,
871
+ );
872
+
859
873
  context.report({
860
874
  node: nodeInfo.node,
861
875
  messageId: 'preferCanonicalArraySlicing',
862
876
  data: { replacement: nodeInfo.replacement },
863
- fix: (fixer) => {
864
- const importFixes =
865
- index === 0 && needsArrImport
866
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
867
- 'Arr',
868
- ])
869
- : [];
870
-
871
- return [
872
- ...importFixes,
873
- fixer.replaceText(nodeInfo.node, nodeInfo.replacement),
874
- ];
875
- },
877
+ fix: writable
878
+ ? (fixer) => [
879
+ ...arrTarget.edits(fixer),
880
+ fixer.replaceText(nodeInfo.node, nodeInfo.replacement),
881
+ ]
882
+ : null,
876
883
  });
877
884
  }
878
885
  },
879
886
  };
880
887
  },
881
888
  } as const;
889
+
890
+ /**
891
+ * How `Arr` is written in this file; see where it is called. A value import of
892
+ * `Arr` comes first, then a namespace import, and only then an edit.
893
+ */
894
+ const resolveArrTarget = (
895
+ sourceCode: TSESLint.SourceCode,
896
+ tsDataForgeImports: readonly TSESTree.ImportDeclaration[],
897
+ ):
898
+ | Readonly<{
899
+ ref: string;
900
+ boundName: string;
901
+ edits: ValueImportBinding['edits'];
902
+ }>
903
+ | undefined => {
904
+ const valueSpecifiers = tsDataForgeImports
905
+ .filter((declaration) => declaration.importKind !== 'type')
906
+ .flatMap((declaration) => declaration.specifiers);
907
+
908
+ const arrImport = valueSpecifiers.find(
909
+ (specifier) =>
910
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
911
+ specifier.importKind !== 'type' &&
912
+ importedNameOf(specifier) === 'Arr',
913
+ );
914
+
915
+ if (arrImport !== undefined) {
916
+ return {
917
+ ref: arrImport.local.name,
918
+ boundName: arrImport.local.name,
919
+ edits: () => [],
920
+ };
921
+ }
922
+
923
+ const namespace = valueSpecifiers.find(
924
+ (specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
925
+ );
926
+
927
+ if (namespace !== undefined) {
928
+ return {
929
+ ref: `${namespace.local.name}.Arr`,
930
+ boundName: namespace.local.name,
931
+ edits: () => [],
932
+ };
933
+ }
934
+
935
+ const binding = bindValueImports(sourceCode, TS_DATA_FORGE_MODULE, ['Arr']);
936
+
937
+ const localName = binding?.localNames[0];
938
+
939
+ return binding === undefined || localName === undefined
940
+ ? undefined
941
+ : { ref: localName, boundName: localName, edits: binding.edits };
942
+ };