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
@@ -3,14 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { Arr } from 'ts-data-forge';
7
- import type { ReadonlyRecord } from 'ts-type-forge';
8
6
  import {
9
- buildImportFixes,
7
+ buildNamedImportFix,
8
+ getArgumentText,
9
+ getImportDeclarations,
10
10
  getImportedLocalName,
11
11
  getNamedImports,
12
- getTsDataForgeImport,
13
- } from './import-utils.mjs';
12
+ } from 'eslint-rule-forge';
13
+ import { Arr } from 'ts-data-forge';
14
+ import type { ReadonlyRecord } from 'ts-type-forge';
15
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
14
16
 
15
17
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
16
18
 
@@ -91,7 +93,10 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
91
93
 
92
94
  const program = sourceCode.ast;
93
95
 
94
- const tsDataForgeImport = getTsDataForgeImport(program);
96
+ const tsDataForgeImports = getImportDeclarations(
97
+ program,
98
+ TS_DATA_FORGE_MODULE,
99
+ );
95
100
 
96
101
  const mut_roundTrips: {
97
102
  node: TSESTree.CallExpression;
@@ -146,7 +151,7 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
146
151
  body.test,
147
152
  );
148
153
 
149
- const someText = `${optionalName}.some(${argumentText(sourceCode.getText(body.kept), body.kept)})`;
154
+ const someText = `${optionalName}.some(${getArgumentText(sourceCode, body.kept)})`;
150
155
 
151
156
  const noneText = `${optionalName}.none`;
152
157
 
@@ -167,12 +172,13 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
167
172
  },
168
173
 
169
174
  'Program:exit': () => {
170
- const namedImports = getNamedImports(tsDataForgeImport);
175
+ const namedImports = getNamedImports(tsDataForgeImports);
171
176
 
172
- const objName = getImportedLocalName(tsDataForgeImport, 'Obj') ?? 'Obj';
177
+ const objName =
178
+ getImportedLocalName(tsDataForgeImports, 'Obj') ?? 'Obj';
173
179
 
174
180
  const optionalName =
175
- getImportedLocalName(tsDataForgeImport, 'Optional') ?? 'Optional';
181
+ getImportedLocalName(tsDataForgeImports, 'Optional') ?? 'Optional';
176
182
 
177
183
  const needsOptional = mut_roundTrips.some(
178
184
  ({ roundTrip }) => roundTrip.body.kind === 'optional',
@@ -191,19 +197,16 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
191
197
  node,
192
198
  messageId: MESSAGE_ID_OF[roundTrip.method],
193
199
  fix: (fixer) => {
194
- const recordText = argumentText(
195
- sourceCode.getText(roundTrip.record),
196
- roundTrip.record,
197
- );
200
+ const recordText = getArgumentText(sourceCode, roundTrip.record);
198
201
 
199
202
  const replacement = `${objName}.${roundTrip.method}(${recordText}, ${buildParams(roundTrip)} => ${buildCallbackBody(roundTrip, optionalName)})`;
200
203
 
201
204
  const importFixes =
202
205
  index === 0 && missingImports.length > 0
203
- ? buildImportFixes(
206
+ ? buildNamedImportFix(
204
207
  fixer,
205
208
  program,
206
- tsDataForgeImport,
209
+ TS_DATA_FORGE_MODULE,
207
210
  missingImports,
208
211
  )
209
212
  : [];
@@ -551,12 +554,6 @@ const conditionalTestText = (
551
554
  ): string =>
552
555
  NEEDS_PARENS_AS_TEST.has(node.type) ? (`(${text})` as const) : text;
553
556
 
554
- /** A comma expression would read as another argument without parentheses. */
555
- const argumentText = (text: string, node: TSESTree.Expression): string =>
556
- node.type === AST_NODE_TYPES.SequenceExpression
557
- ? (`(${text})` as const)
558
- : text;
559
-
560
557
  /**
561
558
  * A concise arrow body cannot start with `{`, nor hold a bare comma. The text
562
559
  * is what decides the first case rather than the node type, because an object
@@ -1,15 +1,18 @@
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 { skipTypeWrappers, toArgumentText } from './ast-utils.mjs';
8
7
  import {
9
- buildImportFixes,
8
+ buildNamedImportFix,
9
+ getArgumentText,
10
+ getImportDeclarations,
10
11
  getNamedImports,
11
- getTsDataForgeImport,
12
- } from './import-utils.mjs';
12
+ skipTypeWrappers,
13
+ } from 'eslint-rule-forge';
14
+ import { Arr } from 'ts-data-forge';
15
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
13
16
 
14
17
  type Options = readonly [];
15
18
 
@@ -35,7 +38,44 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
35
38
 
36
39
  const program = sourceCode.ast;
37
40
 
38
- const tsDataForgeImport = getTsDataForgeImport(program);
41
+ const tsDataForgeImports = getImportDeclarations(
42
+ program,
43
+ TS_DATA_FORGE_MODULE,
44
+ );
45
+
46
+ /**
47
+ * Whether `node` is the same value on every iteration: a literal, or a
48
+ * variable that is never reassigned anywhere (not only in the loop, since
49
+ * a function the loop calls could reassign it). The loop reads its end and
50
+ * its step on every iteration, and `range` reads them once.
51
+ */
52
+ const isStableBound = (
53
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
54
+ wrapped: TSESTree.Node,
55
+ ): boolean => {
56
+ const node = skipTypeWrappers(wrapped);
57
+
58
+ if (node.type === AST_NODE_TYPES.Literal) {
59
+ return true;
60
+ }
61
+
62
+ if (node.type !== AST_NODE_TYPES.Identifier) {
63
+ return false;
64
+ }
65
+
66
+ const variable = ASTUtils.findVariable(
67
+ sourceCode.getScope(node),
68
+ node.name,
69
+ );
70
+
71
+ return (
72
+ variable !== null &&
73
+ variable.defs.length > 0 &&
74
+ variable.references.every(
75
+ (reference) => !reference.isWrite() || reference.init === true,
76
+ )
77
+ );
78
+ };
39
79
 
40
80
  const mut_nodesToFix: {
41
81
  node: TSESTree.ForStatement;
@@ -43,6 +83,7 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
43
83
  begin: string;
44
84
  end: string;
45
85
  step?: string;
86
+ fixable: boolean;
46
87
  }[] = [];
47
88
 
48
89
  return {
@@ -69,6 +110,11 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
69
110
 
70
111
  const beginExpr = declaration.init;
71
112
 
113
+ // `range` takes safe integers only.
114
+ if (isNonIntegerNumberLiteral(beginExpr)) {
115
+ return;
116
+ }
117
+
72
118
  // Check test: i < end. The loop variable and the step are read
73
119
  // through type wrappers: `(i satisfies number) < n` tests `i`, and
74
120
  // `i += 0 as number` still steps by 0.
@@ -121,22 +167,38 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
121
167
  const stepNum = stepValue.value;
122
168
 
123
169
  // Only support positive integer steps
124
- if (typeof stepNum === 'number' && stepNum > 0) {
170
+ if (
171
+ typeof stepNum === 'number' &&
172
+ Number.isSafeInteger(stepNum) &&
173
+ stepNum > 0
174
+ ) {
125
175
  mut_step = stepNum === 1 ? undefined : stepNum.toString();
126
176
  } else {
127
177
  return;
128
178
  }
179
+ } else if (isNegatedLiteral(stepValue)) {
180
+ // `i += -1` under `i < end` never gets there, and `range` with a
181
+ // negative step yields nothing: no fix that type-checks would
182
+ // keep that.
183
+ return;
129
184
  } else {
130
- // Variable step like i += step
131
- mut_step = toArgumentText(node.update.right, sourceCode);
185
+ // Variable step like i += step. Its type is left to the type
186
+ // checker: a step that is not a non-zero safe integer makes the
187
+ // fixed `range(...)` call fail to type-check.
188
+ mut_step = getArgumentText(sourceCode, node.update.right);
132
189
  }
133
190
  } else {
134
191
  return;
135
192
  }
136
193
 
137
- const beginText = toArgumentText(beginExpr, sourceCode);
194
+ // `const i` would not take an assignment anywhere but the update.
195
+ if (isAssignedOutsideUpdate(sourceCode, declaration, node.update)) {
196
+ return;
197
+ }
198
+
199
+ const beginText = getArgumentText(sourceCode, beginExpr);
138
200
 
139
- const endText = toArgumentText(endExpr, sourceCode);
201
+ const endText = getArgumentText(sourceCode, endExpr);
140
202
 
141
203
  mut_nodesToFix.push({
142
204
  node,
@@ -144,17 +206,22 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
144
206
  begin: beginText,
145
207
  end: endText,
146
208
  step: mut_step,
209
+ fixable:
210
+ isStableBound(endExpr) &&
211
+ (node.update.type !== AST_NODE_TYPES.AssignmentExpression ||
212
+ isStableBound(node.update.right)),
147
213
  });
148
214
  },
149
215
  'Program:exit': () => {
150
- const namedImports = getNamedImports(tsDataForgeImport);
216
+ const namedImports = getNamedImports(tsDataForgeImports);
151
217
 
152
218
  const hasRangeImport = namedImports.includes('range');
153
219
 
154
- for (const [
155
- index,
156
- { node, varName, begin, end, step },
157
- ] of mut_nodesToFix.entries()) {
220
+ const importCarrier = mut_nodesToFix.find(({ fixable }) => fixable);
221
+
222
+ for (const entry of mut_nodesToFix) {
223
+ const { node, varName, begin, end, step } = entry;
224
+
158
225
  context.report({
159
226
  node: node.init ?? node,
160
227
  messageId: 'useRangeForLoop',
@@ -163,25 +230,33 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
163
230
  begin,
164
231
  end,
165
232
  },
166
- fix: (fixer) => {
167
- const bodyText = sourceCode.getText(node.body);
233
+ fix: entry.fixable
234
+ ? (fixer) => {
235
+ const bodyText = sourceCode.getText(node.body);
168
236
 
169
- const rangeArgs =
170
- step === undefined
171
- ? `${begin}, ${end}`
172
- : `${begin}, ${end}, ${step}`;
237
+ const rangeArgs =
238
+ step === undefined
239
+ ? `${begin}, ${end}`
240
+ : `${begin}, ${end}, ${step}`;
173
241
 
174
- const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
242
+ const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
175
243
 
176
- const importFixes =
177
- index === 0 && !hasRangeImport
178
- ? buildImportFixes(fixer, program, tsDataForgeImport, [
179
- 'range',
180
- ])
181
- : [];
244
+ // The import fix lives on the first fixable report:
245
+ // `insertTextBefore(program, …)` produces overlapping ranges
246
+ // otherwise.
247
+ const importFixes =
248
+ entry === importCarrier && !hasRangeImport
249
+ ? buildNamedImportFix(
250
+ fixer,
251
+ program,
252
+ TS_DATA_FORGE_MODULE,
253
+ ['range'],
254
+ )
255
+ : [];
182
256
 
183
- return [...importFixes, fixer.replaceText(node, replacement)];
184
- },
257
+ return [...importFixes, fixer.replaceText(node, replacement)];
258
+ }
259
+ : undefined,
185
260
  });
186
261
  }
187
262
  },
@@ -189,3 +264,54 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
189
264
  },
190
265
  defaultOptions: [],
191
266
  } as const;
267
+
268
+ const isNegatedLiteral = (
269
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
270
+ node: TSESTree.Node,
271
+ ): boolean =>
272
+ node.type === AST_NODE_TYPES.UnaryExpression &&
273
+ node.operator === '-' &&
274
+ skipTypeWrappers(node.argument).type === AST_NODE_TYPES.Literal;
275
+
276
+ const isNonIntegerNumberLiteral = (
277
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
278
+ wrapped: TSESTree.Node,
279
+ ): boolean => {
280
+ const node = skipTypeWrappers(wrapped);
281
+
282
+ return (
283
+ node.type === AST_NODE_TYPES.Literal &&
284
+ typeof node.value === 'number' &&
285
+ !Number.isSafeInteger(node.value)
286
+ );
287
+ };
288
+
289
+ /**
290
+ * Whether the loop variable declared by `declaration` is written anywhere but
291
+ * its initializer and the loop's `update`.
292
+ */
293
+ const isAssignedOutsideUpdate = (
294
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
295
+ sourceCode: TSESLint.SourceCode,
296
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
297
+ declaration: TSESTree.VariableDeclarator,
298
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
299
+ update: TSESTree.Expression,
300
+ ): boolean =>
301
+ sourceCode
302
+ .getDeclaredVariables(declaration)
303
+ .flatMap((variable) => variable.references)
304
+ .some(
305
+ (reference) =>
306
+ reference.isWrite() &&
307
+ reference.init !== true &&
308
+ !isWithin(reference.identifier, update),
309
+ );
310
+
311
+ const isWithin = (
312
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
313
+ node: TSESTree.Node,
314
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
315
+ container: TSESTree.Node,
316
+ ): boolean =>
317
+ container.range[0] <= node.range[0] && node.range[1] <= container.range[1];
@@ -205,22 +205,20 @@ describe('prefer-range-for-loop with parenthesized operands', () => {
205
205
  valid: [],
206
206
  invalid: [
207
207
  {
208
- name: 'sequence bounds and step keep their parentheses',
208
+ name: 'a sequence begin keeps its parentheses',
209
209
  code: dedent`
210
210
  import { range } from 'ts-data-forge';
211
211
  declare const n: number;
212
- declare const step: number;
213
212
  declare const log: () => void;
214
- for (let i = (log(), 0); i < (log(), n); i += (log(), step)) {
213
+ for (let i = (log(), 0); i < n; ++i) {
215
214
  console.log(i);
216
215
  }
217
216
  `,
218
217
  output: dedent`
219
218
  import { range } from 'ts-data-forge';
220
219
  declare const n: number;
221
- declare const step: number;
222
220
  declare const log: () => void;
223
- for (const i of range((log(), 0), (log(), n), (log(), step))) {
221
+ for (const i of range((log(), 0), n)) {
224
222
  console.log(i);
225
223
  }
226
224
  `,
@@ -229,3 +227,127 @@ describe('prefer-range-for-loop with parenthesized operands', () => {
229
227
  ],
230
228
  });
231
229
  }, 20000);
230
+
231
+ describe('prefer-range-for-loop where the rewrite would change behavior', () => {
232
+ tester.run('prefer-range-for-loop', preferRangeForLoop, {
233
+ valid: [
234
+ {
235
+ // `const i` could not be assigned.
236
+ name: 'the loop variable assigned in the body',
237
+ code: dedent`
238
+ declare const n: number;
239
+ declare const skip: boolean;
240
+ for (let i = 0; i < n; ++i) {
241
+ if (skip) i += 1;
242
+ }
243
+ for (let j = 0; j < n; ++j) {
244
+ j = n;
245
+ }
246
+ `,
247
+ },
248
+ {
249
+ // `range` takes safe integers only.
250
+ name: 'a step or a begin that is not an integer',
251
+ code: dedent`
252
+ declare const n: number;
253
+ for (let i = 0; i < n; i += 0.5) {
254
+ console.log(i);
255
+ }
256
+ for (let j = 0.5; j < n; ++j) {
257
+ console.log(j);
258
+ }
259
+ `,
260
+ },
261
+ {
262
+ // `i < n` with a negative step never ends; `range` yields nothing.
263
+ name: 'a negative literal step',
264
+ code: dedent`
265
+ declare const n: number;
266
+ for (let i = 0; i < n; i += -1) {
267
+ console.log(i);
268
+ }
269
+ `,
270
+ },
271
+ ],
272
+ invalid: [
273
+ {
274
+ // The loop tests its end on every iteration; `range` reads it once.
275
+ name: 'an end that is not a literal or a never-reassigned variable gets no fix',
276
+ code: dedent`
277
+ declare const xs: number[];
278
+ declare const getN: () => number;
279
+ let m = 3;
280
+ for (let i = 0; i < xs.length; ++i) {
281
+ xs.pop();
282
+ }
283
+ for (let j = 0; j < getN(); ++j) {
284
+ console.log(j);
285
+ }
286
+ for (let k = 0; k < m; ++k) {
287
+ m -= 1;
288
+ }
289
+ `,
290
+ output: null,
291
+ errors: [
292
+ { messageId: 'useRangeForLoop' },
293
+ { messageId: 'useRangeForLoop' },
294
+ { messageId: 'useRangeForLoop' },
295
+ ],
296
+ },
297
+ {
298
+ // The loop adds the step it reads on each iteration; `range` reads it
299
+ // once.
300
+ name: 'a variable step that is reassigned gets no fix',
301
+ code: dedent`
302
+ declare const n: number;
303
+ let step = 1;
304
+ for (let i = 0; i < n; i += step) {
305
+ step *= 2;
306
+ }
307
+ `,
308
+ output: null,
309
+ errors: [{ messageId: 'useRangeForLoop' }],
310
+ },
311
+ {
312
+ // The type checker judges it: a `number` step makes the fixed call
313
+ // fail to type-check.
314
+ name: 'a variable step is fixed without asking its type',
315
+ code: dedent`
316
+ declare const n: number;
317
+ declare const step: number;
318
+ for (let i = 0; i < n; i += step) {
319
+ console.log(i);
320
+ }
321
+ `,
322
+ output: dedent`
323
+ import { range } from 'ts-data-forge';
324
+ declare const n: number;
325
+ declare const step: number;
326
+ for (const i of range(0, n, step)) {
327
+ console.log(i);
328
+ }
329
+ `,
330
+ errors: [{ messageId: 'useRangeForLoop' }],
331
+ },
332
+ {
333
+ name: 'a parameter that is never reassigned is a fixed end',
334
+ code: dedent`
335
+ const f = (n: number): void => {
336
+ for (let i = 0; i < n; ++i) {
337
+ console.log(i);
338
+ }
339
+ };
340
+ `,
341
+ output: dedent`
342
+ import { range } from 'ts-data-forge';
343
+ const f = (n: number): void => {
344
+ for (const i of range(0, n)) {
345
+ console.log(i);
346
+ }
347
+ };
348
+ `,
349
+ errors: [{ messageId: 'useRangeForLoop' }],
350
+ },
351
+ ],
352
+ });
353
+ }, 20000);
@@ -1,6 +1,7 @@
1
1
  import type { ESLintPlugin } from '../types.mjs';
2
2
  import { noSideEffectImport } from './no-side-effect-import.mjs';
3
3
  import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
4
+ import { preferArrGenerate } from './prefer-arr-generate.mjs';
4
5
  import { preferArrIsArray } from './prefer-arr-is-array.mjs';
5
6
  import { preferArrScan } from './prefer-arr-scan.mjs';
6
7
  import { preferArrSum } from './prefer-arr-sum.mjs';
@@ -21,6 +22,7 @@ export const tsDataForgeRules = {
21
22
  'prefer-canonical-array-slicing': preferCanonicalArraySlicing,
22
23
  'prefer-canonical-length-guard': preferCanonicalLengthGuard,
23
24
  'prefer-canonical-length-cast': preferCanonicalLengthCast,
25
+ 'prefer-arr-generate': preferArrGenerate,
24
26
  'prefer-arr-is-array': preferArrIsArray,
25
27
  'prefer-arr-scan': preferArrScan,
26
28
  'prefer-arr-sum': preferArrSum,
@@ -1,30 +0,0 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- import type { DeepReadonly } from 'ts-type-forge';
3
- export declare const getTsDataForgeImport: (program: TSESTree.Program) => TSESTree.ImportDeclaration | undefined;
4
- export declare const getNamedImports: (node: DeepReadonly<TSESTree.ImportDeclaration> | undefined) => readonly string[];
5
- /**
6
- * Returns the in-scope local name a given canonical export is imported as
7
- * (handling aliases such as `import { isX as y }` → `'y'`), or `undefined` when
8
- * it is not imported. Used so autofixes reference the binding that actually
9
- * exists rather than the canonical name.
10
- */
11
- export declare const getImportedLocalName: (node: DeepReadonly<TSESTree.ImportDeclaration> | undefined, importedName: string) => string | undefined;
12
- type ResolvedCallee = Readonly<{
13
- /** The canonical (imported) ts-data-forge name, regardless of local alias. */
14
- canonicalName: string;
15
- /** The identifier node to rewrite when replacing the callee name. */
16
- propertyNode: TSESTree.Node;
17
- /** Whether the call was made through a namespace import (`tf.isX(...)`). */
18
- isNamespace: boolean;
19
- }>;
20
- /**
21
- * Builds a resolver that maps a call expression's callee to the canonical
22
- * ts-data-forge function it refers to. Handles named imports (including aliases
23
- * such as `import { isX as y }`) and namespace imports (`import * as tf` →
24
- * `tf.isX(...)`). Returns `undefined` for any callee that does not reference the
25
- * given ts-data-forge import.
26
- */
27
- export declare const buildCalleeResolver: (importDecl: TSESTree.ImportDeclaration | undefined) => ((callee: DeepReadonly<TSESTree.Expression>) => ResolvedCallee | undefined);
28
- export declare const buildImportFixes: (fixer: TSESLint.RuleFixer, program: TSESTree.Program, _tsDataForgeImport: TSESTree.ImportDeclaration | undefined, requiredNames: readonly string[]) => readonly TSESLint.RuleFix[];
29
- export {};
30
- //# sourceMappingURL=import-utils.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAMlD,eAAO,MAAM,oBAAoB,YACtB,QAAQ,CAAC,OAAO,KACxB,QAAQ,CAAC,iBAAiB,GAAG,SAK7B,CAAC;AAEJ,eAAO,MAAM,eAAe,SACpB,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,KACzD,SAAS,MAAM,EAiBjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,SACzB,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,gBAC5C,MAAM,KACnB,MAAM,GAAG,SAOG,CAAC;AAEhB,KAAK,cAAc,GAAG,QAAQ,CAAC;IAC7B,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;IAC5B,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,eAClB,QAAQ,CAAC,iBAAiB,GAAG,SAAS,KACjD,CAAC,CACF,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,KACtC,cAAc,GAAG,SAAS,CAsE9B,CAAC;AAOF,eAAO,MAAM,gBAAgB,UACpB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,sBACL,QAAQ,CAAC,iBAAiB,GAAG,SAAS,iBAC3C,SAAS,MAAM,EAAE,KAC/B,SAAS,QAAQ,CAAC,OAAO,EAa3B,CAAC"}
@@ -1,98 +0,0 @@
1
- import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
- import { isTypeWrapper } from './ast-utils.mjs';
3
- import { TS_DATA_FORGE_MODULE } from './constants.mjs';
4
-
5
- export const getTsDataForgeImport = (program) => program.body.find((node) => node.type === AST_NODE_TYPES.ImportDeclaration &&
6
- node.source.value === TS_DATA_FORGE_MODULE);
7
- export const getNamedImports = (node) => {
8
- if (node === undefined) {
9
- return [];
10
- }
11
- return node.specifiers.flatMap((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier
12
- ? (() => {
13
- const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier
14
- ? specifier.imported.name
15
- : specifier.imported.value;
16
- return typeof importedName === 'string' ? [importedName] : [];
17
- })()
18
- : []);
19
- };
20
-
21
-
22
-
23
-
24
-
25
-
26
- export const getImportedLocalName = (node, importedName) => node?.specifiers.find((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier &&
27
- (specifier.imported.type === AST_NODE_TYPES.Identifier
28
- ? specifier.imported.name
29
- : specifier.imported.value) === importedName)?.local.name;
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
- export const buildCalleeResolver = (importDecl) => {
38
- const specifiers = importDecl?.specifiers ?? [];
39
- const namespaceName = specifiers.find((specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier)?.local.name;
40
- const localToCanonical = new Map(specifiers.flatMap((specifier) => {
41
- if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
42
- return [];
43
- }
44
- const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier
45
- ? specifier.imported.name
46
- : specifier.imported.value;
47
- return typeof importedName === 'string'
48
- ? [[specifier.local.name, importedName]]
49
- : [];
50
- }));
51
-
52
-
53
-
54
- const resolve = (callee) => {
55
- if (isTypeWrapper(callee)) {
56
- return resolve(callee.expression);
57
- }
58
- if (callee.type === AST_NODE_TYPES.Identifier) {
59
- const canonicalName = localToCanonical.get(callee.name);
60
- return canonicalName === undefined
61
- ? undefined
62
- : {
63
- canonicalName,
64
-
65
- propertyNode: callee,
66
- isNamespace: false,
67
- };
68
- }
69
- if (namespaceName === undefined ||
70
- callee.type !== AST_NODE_TYPES.MemberExpression ||
71
- callee.computed ||
72
- callee.property.type !== AST_NODE_TYPES.Identifier) {
73
- return undefined;
74
- }
75
- const object = skipReadonlyTypeWrappers(callee.object);
76
- return object.type === AST_NODE_TYPES.Identifier &&
77
- object.name === namespaceName
78
- ? {
79
- canonicalName: callee.property.name,
80
-
81
- propertyNode: callee.property,
82
- isNamespace: true,
83
- }
84
- : undefined;
85
- };
86
- return resolve;
87
- };
88
- const skipReadonlyTypeWrappers = (node) => isTypeWrapper(node) ? skipReadonlyTypeWrappers(node.expression) : node;
89
- export const buildImportFixes = (fixer, program, _tsDataForgeImport, requiredNames) => {
90
- const specifierText = requiredNames.join(', ');
91
- const importStatement = `import { ${specifierText} } from '${TS_DATA_FORGE_MODULE}';`;
92
- const newLine = '\n';
93
- const insertionText = `${importStatement}${newLine}`;
94
-
95
-
96
- return [fixer.insertTextBefore(program, insertionText)];
97
- };
98
- //# sourceMappingURL=import-utils.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"import-utils.mjs","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"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"}