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.
- package/README.md +3 -3
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +5 -5
- package/dist/rules/ast-utils.d.mts +8 -9
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +3 -4
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-binding.d.mts +38 -0
- package/dist/rules/import-binding.d.mts.map +1 -0
- package/dist/rules/import-binding.mjs +53 -0
- package/dist/rules/import-binding.mjs.map +1 -0
- package/dist/rules/mutable-result.mjs +37 -37
- package/dist/rules/mutable-result.mjs.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +2 -2
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
- package/dist/rules/prefer-arr-generate.mjs +146 -136
- package/dist/rules/prefer-arr-generate.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +9 -18
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +16 -21
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +38 -43
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +19 -26
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +49 -16
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.d.mts +2 -2
- package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
- package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +18 -24
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +2 -2
- package/package.json +6 -6
- package/src/plugin.mts +4 -3
- package/src/rule-types.mts +5 -5
- package/src/rules/ast-utils.mts +14 -15
- package/src/rules/import-binding.mts +94 -0
- package/src/rules/mutable-result.mts +56 -56
- package/src/rules/no-unnecessary-type-guard.mts +203 -207
- package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
- package/src/rules/prefer-arr-generate.mts +222 -214
- package/src/rules/prefer-arr-generate.test.mts +490 -133
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-max-length-array.mts +23 -36
- package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-min-length-array.mts +23 -36
- package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-non-empty.mts +17 -34
- package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
- package/src/rules/prefer-arr-scan.mts +17 -42
- package/src/rules/prefer-arr-scan.test.mts +55 -0
- package/src/rules/prefer-arr-sum.mts +22 -36
- package/src/rules/prefer-arr-uniq.mts +51 -62
- package/src/rules/prefer-arr-uniq.test.mts +46 -20
- package/src/rules/prefer-as-int.mts +27 -52
- package/src/rules/prefer-as-int.test.mts +79 -29
- package/src/rules/prefer-canonical-array-slicing.mts +101 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
- package/src/rules/prefer-canonical-length-cast.mts +29 -29
- package/src/rules/prefer-canonical-length-guard.mts +2 -2
- package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
- package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
- package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
- package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
- package/src/rules/prefer-pipe-map-nullable.mts +17 -19
- package/src/rules/prefer-range-for-loop.mts +37 -61
- package/src/rules/prefer-range-for-loop.test.mts +2 -2
|
@@ -3,16 +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
|
-
getImportedLocalName,
|
|
11
|
-
getNamedImports,
|
|
12
|
-
} from 'eslint-rule-forge';
|
|
6
|
+
import { getArgumentText } from 'eslint-rule-forge';
|
|
13
7
|
import { Arr } from 'ts-data-forge';
|
|
14
8
|
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
15
|
-
import {
|
|
9
|
+
import { tsDataForgeValuesAt } from './import-binding.mjs';
|
|
16
10
|
|
|
17
11
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
18
12
|
|
|
@@ -92,13 +86,6 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
|
|
|
92
86
|
create: (context) => {
|
|
93
87
|
const sourceCode = context.sourceCode;
|
|
94
88
|
|
|
95
|
-
const program = sourceCode.ast;
|
|
96
|
-
|
|
97
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
98
|
-
program,
|
|
99
|
-
TS_DATA_FORGE_MODULE,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
89
|
const mut_roundTrips: {
|
|
103
90
|
node: TSESTree.CallExpression;
|
|
104
91
|
roundTrip: RoundTrip;
|
|
@@ -173,47 +160,42 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
|
|
|
173
160
|
},
|
|
174
161
|
|
|
175
162
|
'Program:exit': () => {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
const objName =
|
|
179
|
-
getImportedLocalName(tsDataForgeImports, 'Obj') ?? 'Obj';
|
|
163
|
+
const objAt = tsDataForgeValuesAt(sourceCode, ['Obj']).at;
|
|
180
164
|
|
|
181
|
-
const
|
|
182
|
-
|
|
165
|
+
const objOptionalAt = tsDataForgeValuesAt(sourceCode, [
|
|
166
|
+
'Obj',
|
|
167
|
+
'Optional',
|
|
168
|
+
]).at;
|
|
183
169
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
170
|
+
// Every fix brings the names it writes into scope itself, so that one
|
|
171
|
+
// applied alone (an editor's quick fix) is complete.
|
|
172
|
+
for (const { node, roundTrip } of mut_roundTrips) {
|
|
173
|
+
const binding = (
|
|
174
|
+
roundTrip.body.kind === 'optional' ? objOptionalAt : objAt
|
|
175
|
+
)(node);
|
|
187
176
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
(name !== 'Optional' || needsOptional) &&
|
|
191
|
-
!namedImports.includes(name),
|
|
192
|
-
);
|
|
177
|
+
const [objName, optionalName = 'Optional'] =
|
|
178
|
+
binding?.localNames ?? [];
|
|
193
179
|
|
|
194
|
-
// Note: the import is added by the first fix only, so that several
|
|
195
|
-
// fixes in one file do not each insert it at the same position.
|
|
196
|
-
for (const [index, { node, roundTrip }] of mut_roundTrips.entries()) {
|
|
197
180
|
context.report({
|
|
198
181
|
node,
|
|
199
182
|
messageId: MESSAGE_ID_OF[roundTrip.method],
|
|
200
|
-
fix:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
},
|
|
183
|
+
fix:
|
|
184
|
+
binding === undefined || objName === undefined
|
|
185
|
+
? null
|
|
186
|
+
: (fixer) => {
|
|
187
|
+
const recordText = getArgumentText(
|
|
188
|
+
sourceCode,
|
|
189
|
+
roundTrip.record,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const replacement = `${objName}.${roundTrip.method}(${recordText}, ${buildParams(roundTrip)} => ${buildCallbackBody(roundTrip, optionalName)})`;
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
...binding.edits(fixer),
|
|
196
|
+
fixer.replaceText(node, replacement),
|
|
197
|
+
];
|
|
198
|
+
},
|
|
217
199
|
});
|
|
218
200
|
}
|
|
219
201
|
},
|
|
@@ -221,6 +203,127 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
|
|
|
221
203
|
},
|
|
222
204
|
} as const;
|
|
223
205
|
|
|
206
|
+
const isElidedOrIdentifier = (
|
|
207
|
+
element: TSESTree.DestructuringPattern | null | undefined,
|
|
208
|
+
): element is TSESTree.Identifier | null | undefined =>
|
|
209
|
+
element === null ||
|
|
210
|
+
element === undefined ||
|
|
211
|
+
element.type === AST_NODE_TYPES.Identifier;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Reads the `[key, value]` destructuring the entry callback binds. Anything
|
|
215
|
+
* beyond a plain binding — a default, a rest element, a nested pattern — is
|
|
216
|
+
* rejected, because the rewritten callback receives the value and the key
|
|
217
|
+
* directly rather than an entry tuple.
|
|
218
|
+
*/
|
|
219
|
+
const parseEntryPattern = (
|
|
220
|
+
pattern: TSESTree.ArrayPattern,
|
|
221
|
+
): EntryParams | undefined => {
|
|
222
|
+
if (pattern.elements.length > 2) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const [keyElement, valueElement] = pattern.elements;
|
|
227
|
+
|
|
228
|
+
return isElidedOrIdentifier(keyElement) && isElidedOrIdentifier(valueElement)
|
|
229
|
+
? {
|
|
230
|
+
keyParam: keyElement ?? undefined,
|
|
231
|
+
valueParam: valueElement ?? undefined,
|
|
232
|
+
}
|
|
233
|
+
: undefined;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const isConstAssertion = (node: TSESTree.TSAsExpression): boolean =>
|
|
237
|
+
node.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference &&
|
|
238
|
+
node.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier &&
|
|
239
|
+
node.typeAnnotation.typeName.name === 'const';
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Looks through the `as const` / `satisfies` an entry tuple usually carries.
|
|
243
|
+
* Any other assertion is left in place, so that a tuple asserted to a wider
|
|
244
|
+
* type than the callback returns is not reported: `Obj` would infer the
|
|
245
|
+
* narrower one and the rewrite would not type-check.
|
|
246
|
+
*/
|
|
247
|
+
const stripTypeWrappers = (node: TSESTree.Expression): TSESTree.Expression =>
|
|
248
|
+
node.type === AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
249
|
+
(node.type === AST_NODE_TYPES.TSAsExpression && isConstAssertion(node))
|
|
250
|
+
? stripTypeWrappers(node.expression)
|
|
251
|
+
: node;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Matches `[k, value]` — an entry rebuilt under the very key it came from,
|
|
255
|
+
* which is the only shape `Obj.map` / `Obj.filterMap` can express. An entry
|
|
256
|
+
* built under a different key is left alone.
|
|
257
|
+
*/
|
|
258
|
+
const parseKeptEntry = (
|
|
259
|
+
node: TSESTree.Expression,
|
|
260
|
+
keyParam: TSESTree.Identifier | undefined,
|
|
261
|
+
): KeptEntry | undefined => {
|
|
262
|
+
if (keyParam === undefined) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tuple = stripTypeWrappers(node);
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
tuple.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
270
|
+
!Arr.isFixedLengthTuple(2, tuple.elements)
|
|
271
|
+
) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const [key, value] = tuple.elements;
|
|
276
|
+
|
|
277
|
+
return key !== null &&
|
|
278
|
+
value !== null &&
|
|
279
|
+
key.type === AST_NODE_TYPES.Identifier &&
|
|
280
|
+
value.type !== AST_NODE_TYPES.SpreadElement &&
|
|
281
|
+
key.name === keyParam.name
|
|
282
|
+
? { key, value }
|
|
283
|
+
: undefined;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Matches `[[k, value]]` — the `flatMap` branch that keeps an entry. */
|
|
287
|
+
const parseSingletonEntry = (
|
|
288
|
+
node: TSESTree.Expression,
|
|
289
|
+
keyParam: TSESTree.Identifier | undefined,
|
|
290
|
+
): KeptEntry | undefined => {
|
|
291
|
+
const array = stripTypeWrappers(node);
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
array.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
295
|
+
!Arr.isFixedLengthTuple(1, array.elements)
|
|
296
|
+
) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const [entry] = array.elements;
|
|
301
|
+
|
|
302
|
+
return entry === null || entry.type === AST_NODE_TYPES.SpreadElement
|
|
303
|
+
? undefined
|
|
304
|
+
: parseKeptEntry(entry, keyParam);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/** Matches `[]` — the `flatMap` branch that drops an entry. */
|
|
308
|
+
const isEmptyArray = (node: TSESTree.Expression): boolean => {
|
|
309
|
+
const array = stripTypeWrappers(node);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
array.type === AST_NODE_TYPES.ArrayExpression && array.elements.length === 0
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const isObjectStaticCall = (
|
|
317
|
+
node: TSESTree.CallExpression,
|
|
318
|
+
name: 'entries' | 'fromEntries',
|
|
319
|
+
): boolean =>
|
|
320
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
321
|
+
!node.callee.computed &&
|
|
322
|
+
node.callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
323
|
+
node.callee.object.name === 'Object' &&
|
|
324
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
325
|
+
node.callee.property.name === name;
|
|
326
|
+
|
|
224
327
|
/**
|
|
225
328
|
* Matches `Object.fromEntries(Object.entries(record).map|filter|flatMap(cb))`
|
|
226
329
|
* and reports which `Obj` function replaces it, or `undefined` when the call is
|
|
@@ -377,133 +480,12 @@ type EntryParams = Readonly<{
|
|
|
377
480
|
valueParam: TSESTree.Identifier | undefined;
|
|
378
481
|
}>;
|
|
379
482
|
|
|
380
|
-
/**
|
|
381
|
-
* Reads the `[key, value]` destructuring the entry callback binds. Anything
|
|
382
|
-
* beyond a plain binding — a default, a rest element, a nested pattern — is
|
|
383
|
-
* rejected, because the rewritten callback receives the value and the key
|
|
384
|
-
* directly rather than an entry tuple.
|
|
385
|
-
*/
|
|
386
|
-
const parseEntryPattern = (
|
|
387
|
-
pattern: TSESTree.ArrayPattern,
|
|
388
|
-
): EntryParams | undefined => {
|
|
389
|
-
if (pattern.elements.length > 2) {
|
|
390
|
-
return undefined;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const [keyElement, valueElement] = pattern.elements;
|
|
394
|
-
|
|
395
|
-
return isElidedOrIdentifier(keyElement) && isElidedOrIdentifier(valueElement)
|
|
396
|
-
? {
|
|
397
|
-
keyParam: keyElement ?? undefined,
|
|
398
|
-
valueParam: valueElement ?? undefined,
|
|
399
|
-
}
|
|
400
|
-
: undefined;
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const isElidedOrIdentifier = (
|
|
404
|
-
element: TSESTree.DestructuringPattern | null | undefined,
|
|
405
|
-
): element is TSESTree.Identifier | null | undefined =>
|
|
406
|
-
element === null ||
|
|
407
|
-
element === undefined ||
|
|
408
|
-
element.type === AST_NODE_TYPES.Identifier;
|
|
409
|
-
|
|
410
483
|
type KeptEntry = Readonly<{
|
|
411
484
|
/** The key identifier that only rebuilds the entry, dropped by the rewrite. */
|
|
412
485
|
key: TSESTree.Identifier;
|
|
413
486
|
value: TSESTree.Expression;
|
|
414
487
|
}>;
|
|
415
488
|
|
|
416
|
-
/**
|
|
417
|
-
* Matches `[k, value]` — an entry rebuilt under the very key it came from,
|
|
418
|
-
* which is the only shape `Obj.map` / `Obj.filterMap` can express. An entry
|
|
419
|
-
* built under a different key is left alone.
|
|
420
|
-
*/
|
|
421
|
-
const parseKeptEntry = (
|
|
422
|
-
node: TSESTree.Expression,
|
|
423
|
-
keyParam: TSESTree.Identifier | undefined,
|
|
424
|
-
): KeptEntry | undefined => {
|
|
425
|
-
if (keyParam === undefined) {
|
|
426
|
-
return undefined;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const tuple = stripTypeWrappers(node);
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
tuple.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
433
|
-
!Arr.isFixedLengthTuple(2, tuple.elements)
|
|
434
|
-
) {
|
|
435
|
-
return undefined;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const [key, value] = tuple.elements;
|
|
439
|
-
|
|
440
|
-
return key !== null &&
|
|
441
|
-
value !== null &&
|
|
442
|
-
key.type === AST_NODE_TYPES.Identifier &&
|
|
443
|
-
value.type !== AST_NODE_TYPES.SpreadElement &&
|
|
444
|
-
key.name === keyParam.name
|
|
445
|
-
? { key, value }
|
|
446
|
-
: undefined;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
/** Matches `[[k, value]]` — the `flatMap` branch that keeps an entry. */
|
|
450
|
-
const parseSingletonEntry = (
|
|
451
|
-
node: TSESTree.Expression,
|
|
452
|
-
keyParam: TSESTree.Identifier | undefined,
|
|
453
|
-
): KeptEntry | undefined => {
|
|
454
|
-
const array = stripTypeWrappers(node);
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
array.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
458
|
-
!Arr.isFixedLengthTuple(1, array.elements)
|
|
459
|
-
) {
|
|
460
|
-
return undefined;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const [entry] = array.elements;
|
|
464
|
-
|
|
465
|
-
return entry === null || entry.type === AST_NODE_TYPES.SpreadElement
|
|
466
|
-
? undefined
|
|
467
|
-
: parseKeptEntry(entry, keyParam);
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
/** Matches `[]` — the `flatMap` branch that drops an entry. */
|
|
471
|
-
const isEmptyArray = (node: TSESTree.Expression): boolean => {
|
|
472
|
-
const array = stripTypeWrappers(node);
|
|
473
|
-
|
|
474
|
-
return (
|
|
475
|
-
array.type === AST_NODE_TYPES.ArrayExpression && array.elements.length === 0
|
|
476
|
-
);
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Looks through the `as const` / `satisfies` an entry tuple usually carries.
|
|
481
|
-
* Any other assertion is left in place, so that a tuple asserted to a wider
|
|
482
|
-
* type than the callback returns is not reported: `Obj` would infer the
|
|
483
|
-
* narrower one and the rewrite would not type-check.
|
|
484
|
-
*/
|
|
485
|
-
const stripTypeWrappers = (node: TSESTree.Expression): TSESTree.Expression =>
|
|
486
|
-
node.type === AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
487
|
-
(node.type === AST_NODE_TYPES.TSAsExpression && isConstAssertion(node))
|
|
488
|
-
? stripTypeWrappers(node.expression)
|
|
489
|
-
: node;
|
|
490
|
-
|
|
491
|
-
const isConstAssertion = (node: TSESTree.TSAsExpression): boolean =>
|
|
492
|
-
node.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference &&
|
|
493
|
-
node.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier &&
|
|
494
|
-
node.typeAnnotation.typeName.name === 'const';
|
|
495
|
-
|
|
496
|
-
const isObjectStaticCall = (
|
|
497
|
-
node: TSESTree.CallExpression,
|
|
498
|
-
name: 'entries' | 'fromEntries',
|
|
499
|
-
): boolean =>
|
|
500
|
-
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
501
|
-
!node.callee.computed &&
|
|
502
|
-
node.callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
503
|
-
node.callee.object.name === 'Object' &&
|
|
504
|
-
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
505
|
-
node.callee.property.name === name;
|
|
506
|
-
|
|
507
489
|
/**
|
|
508
490
|
* `true` when the parameter is still referenced once the identifiers in
|
|
509
491
|
* `dropped` — the ones that only rebuilt the entry key — are discounted.
|
|
@@ -308,6 +308,38 @@ describe('prefer-obj-over-entries-round-trip', () => {
|
|
|
308
308
|
`,
|
|
309
309
|
errors: [{ messageId: 'useObjFilterMap' }],
|
|
310
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: 'adds Obj and Optional to one existing import',
|
|
313
|
+
code: dedent`
|
|
314
|
+
import { asInt } from 'ts-data-forge';
|
|
315
|
+
const scores = { alice: 1, bob: 2 };
|
|
316
|
+
const scaled = Object.fromEntries(
|
|
317
|
+
Object.entries(scores).flatMap(([k, v]) =>
|
|
318
|
+
v > 1 ? [[k, v * 10]] : [],
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
`,
|
|
322
|
+
output: dedent`
|
|
323
|
+
import { asInt, Obj, Optional } from 'ts-data-forge';
|
|
324
|
+
const scores = { alice: 1, bob: 2 };
|
|
325
|
+
const scaled = Obj.filterMap(scores, (v) => v > 1 ? Optional.some(v * 10) : Optional.none);
|
|
326
|
+
`,
|
|
327
|
+
errors: [{ messageId: 'useObjFilterMap' }],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'no fix when an import type binds Optional',
|
|
331
|
+
code: dedent`
|
|
332
|
+
import type { Optional } from 'ts-data-forge';
|
|
333
|
+
const scores = { alice: 1, bob: 2 };
|
|
334
|
+
const scaled = Object.fromEntries(
|
|
335
|
+
Object.entries(scores).flatMap(([k, v]) =>
|
|
336
|
+
v > 1 ? [[k, v * 10]] : [],
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
`,
|
|
340
|
+
output: null,
|
|
341
|
+
errors: [{ messageId: 'useObjFilterMap' }],
|
|
342
|
+
},
|
|
311
343
|
{
|
|
312
344
|
name: 'handles a flatMap whose branches are the other way round',
|
|
313
345
|
code: dedent`
|
|
@@ -368,12 +400,22 @@ describe('prefer-obj-over-entries-round-trip', () => {
|
|
|
368
400
|
Object.entries(scores).filter(([, v]) => v > 1),
|
|
369
401
|
);
|
|
370
402
|
`,
|
|
371
|
-
output:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
output: [
|
|
404
|
+
dedent`
|
|
405
|
+
import { Obj } from 'ts-data-forge';
|
|
406
|
+
const scores = { alice: 1, bob: 2 };
|
|
407
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
408
|
+
const passing = Object.fromEntries(
|
|
409
|
+
Object.entries(scores).filter(([, v]) => v > 1),
|
|
410
|
+
);
|
|
411
|
+
`,
|
|
412
|
+
dedent`
|
|
413
|
+
import { Obj } from 'ts-data-forge';
|
|
414
|
+
const scores = { alice: 1, bob: 2 };
|
|
415
|
+
const doubled = Obj.map(scores, (v) => v * 2);
|
|
416
|
+
const passing = Obj.filter(scores, (v) => v > 1);
|
|
417
|
+
`,
|
|
418
|
+
],
|
|
377
419
|
errors: [{ messageId: 'useObjMap' }, { messageId: 'useObjFilter' }],
|
|
378
420
|
},
|
|
379
421
|
{
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildCalleeResolver,
|
|
8
8
|
getArgumentText,
|
|
9
9
|
getImportDeclarations,
|
|
10
|
+
hasCommentWithin,
|
|
10
11
|
hasForeignBinding,
|
|
11
12
|
isTypeWrapper,
|
|
12
13
|
skipTypeWrappers,
|
|
@@ -33,8 +34,8 @@ type MessageIds = 'useMapNullable' | 'suggestMapNullable';
|
|
|
33
34
|
* first infers `'a'` and the second `string`, and `(v) => [v, 1]` becomes
|
|
34
35
|
* `number[]` instead of `readonly [number, 1]`. The fix is therefore applied
|
|
35
36
|
* automatically only when the function's return type is not inferred, or is
|
|
36
|
-
* inferred with nothing such a widening could change; otherwise, and whenever
|
|
37
|
-
*
|
|
37
|
+
* inferred with nothing such a widening could change; otherwise, and whenever a
|
|
38
|
+
* comment or a type wrapper around `mapNullable` would be dropped, it is
|
|
38
39
|
* offered as a suggestion.
|
|
39
40
|
*/
|
|
40
41
|
export const preferPipeMapNullable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
@@ -80,8 +81,8 @@ export const preferPipeMapNullable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
80
81
|
const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
* Whether `callee` calls the export `name` of the module `resolve`
|
|
84
|
-
*
|
|
84
|
+
* Whether `callee` calls the export `name` of the module `resolve` reads,
|
|
85
|
+
* rather than a local binding that shadows the import.
|
|
85
86
|
*/
|
|
86
87
|
const calls = (
|
|
87
88
|
resolve: ReturnType<typeof buildCalleeResolver>,
|
|
@@ -169,9 +170,9 @@ export const preferPipeMapNullable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// The `B | undefined` that `mapNullable(fn)` returns, with `B` as the
|
|
172
|
-
// `const` type parameter inferred it. The function's own return type
|
|
173
|
-
//
|
|
174
|
-
//
|
|
173
|
+
// `const` type parameter inferred it. The function's own return type is
|
|
174
|
+
// no guide: for `(v) => ({ kind: 'n', v })` it is already widened to
|
|
175
|
+
// `{ kind: string; v: number }`, while `B` keeps `'n'`.
|
|
175
176
|
const signatures = checker
|
|
176
177
|
.getTypeAtLocation(esTreeNodeToTSNodeMap.get(mapNullableCall))
|
|
177
178
|
.getCallSignatures();
|
|
@@ -238,14 +239,11 @@ export const preferPipeMapNullable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
238
239
|
[mapFn.range[1], mapNullableCall.range[1]],
|
|
239
240
|
] as const;
|
|
240
241
|
|
|
241
|
-
const dropsComment =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
start <= comment.range[0] && comment.range[1] <= end,
|
|
247
|
-
),
|
|
248
|
-
);
|
|
242
|
+
const dropsComment = hasCommentWithin(
|
|
243
|
+
sourceCode,
|
|
244
|
+
mapNullableCall,
|
|
245
|
+
droppedRanges,
|
|
246
|
+
);
|
|
249
247
|
|
|
250
248
|
const autofixable =
|
|
251
249
|
!dropsComment &&
|
|
@@ -315,11 +313,11 @@ const MAX_DEPTH = 4;
|
|
|
315
313
|
|
|
316
314
|
/**
|
|
317
315
|
* Whether `type`, inferred for a `const` type parameter, has a part a plain
|
|
318
|
-
* type parameter would infer differently: a literal (`'a'` against `string`)
|
|
319
|
-
*
|
|
316
|
+
* type parameter would infer differently: a literal (`'a'` against `string`) or
|
|
317
|
+
* a tuple (`readonly [number, 1]` against `number[]`), at the top level or
|
|
320
318
|
* inside an array, a generic type's arguments or an object literal's
|
|
321
|
-
* properties. Conservative: a declared literal type, which neither widens,
|
|
322
|
-
*
|
|
319
|
+
* properties. Conservative: a declared literal type, which neither widens, also
|
|
320
|
+
* counts.
|
|
323
321
|
*/
|
|
324
322
|
const hasConstInferredPart = (
|
|
325
323
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -4,15 +4,9 @@ import {
|
|
|
4
4
|
type TSESLint,
|
|
5
5
|
type TSESTree,
|
|
6
6
|
} from '@typescript-eslint/utils';
|
|
7
|
-
import {
|
|
8
|
-
buildNamedImportFix,
|
|
9
|
-
getArgumentText,
|
|
10
|
-
getImportDeclarations,
|
|
11
|
-
getNamedImports,
|
|
12
|
-
skipTypeWrappers,
|
|
13
|
-
} from 'eslint-rule-forge';
|
|
7
|
+
import { getArgumentText, skipTypeWrappers } from 'eslint-rule-forge';
|
|
14
8
|
import { Arr } from 'ts-data-forge';
|
|
15
|
-
import {
|
|
9
|
+
import { tsDataForgeValueAt } from './import-binding.mjs';
|
|
16
10
|
|
|
17
11
|
type Options = readonly [];
|
|
18
12
|
|
|
@@ -37,17 +31,10 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
37
31
|
create: (context) => {
|
|
38
32
|
const sourceCode = context.sourceCode;
|
|
39
33
|
|
|
40
|
-
const program = sourceCode.ast;
|
|
41
|
-
|
|
42
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
43
|
-
program,
|
|
44
|
-
TS_DATA_FORGE_MODULE,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
34
|
/**
|
|
48
35
|
* Whether `node` is the same value on every iteration: a literal, or a
|
|
49
|
-
* variable that is never reassigned anywhere (not only in the loop, since
|
|
50
|
-
*
|
|
36
|
+
* variable that is never reassigned anywhere (not only in the loop, since a
|
|
37
|
+
* function the loop calls could reassign it). The loop reads its end and
|
|
51
38
|
* its step on every iteration, and `range` reads them once.
|
|
52
39
|
*/
|
|
53
40
|
const isStableBound = (
|
|
@@ -116,8 +103,8 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
116
103
|
return;
|
|
117
104
|
}
|
|
118
105
|
|
|
119
|
-
// Check test: i < end. The loop variable and the step are read
|
|
120
|
-
//
|
|
106
|
+
// Check test: i < end. The loop variable and the step are read through
|
|
107
|
+
// type wrappers: `(i satisfies number) < n` tests `i`, and
|
|
121
108
|
// `i += 0 as number` still steps by 0.
|
|
122
109
|
if (
|
|
123
110
|
node.test?.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
@@ -179,8 +166,8 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
179
166
|
}
|
|
180
167
|
} else if (isNegatedLiteral(stepValue)) {
|
|
181
168
|
// `i += -1` under `i < end` never gets there, and `range` with a
|
|
182
|
-
// negative step yields nothing: no fix that type-checks would
|
|
183
|
-
//
|
|
169
|
+
// negative step yields nothing: no fix that type-checks would keep
|
|
170
|
+
// that.
|
|
184
171
|
return;
|
|
185
172
|
} else {
|
|
186
173
|
// Variable step like i += step. Its type is left to the type
|
|
@@ -214,15 +201,13 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
214
201
|
});
|
|
215
202
|
},
|
|
216
203
|
'Program:exit': () => {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
const hasRangeImport = namedImports.includes('range');
|
|
220
|
-
|
|
221
|
-
const importCarrier = mut_nodesToFix.find(({ fixable }) => fixable);
|
|
204
|
+
const rangeAt = tsDataForgeValueAt(sourceCode, 'range');
|
|
222
205
|
|
|
223
206
|
for (const entry of mut_nodesToFix) {
|
|
224
207
|
const { node, varName, begin, end, step } = entry;
|
|
225
208
|
|
|
209
|
+
const range = entry.fixable ? rangeAt(node) : undefined;
|
|
210
|
+
|
|
226
211
|
context.report({
|
|
227
212
|
node: node.init ?? node,
|
|
228
213
|
messageId: 'useRangeForLoop',
|
|
@@ -231,33 +216,24 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
231
216
|
begin,
|
|
232
217
|
end,
|
|
233
218
|
},
|
|
234
|
-
fix:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
program,
|
|
253
|
-
TS_DATA_FORGE_MODULE,
|
|
254
|
-
['range'],
|
|
255
|
-
)
|
|
256
|
-
: [];
|
|
257
|
-
|
|
258
|
-
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
259
|
-
}
|
|
260
|
-
: undefined,
|
|
219
|
+
fix:
|
|
220
|
+
range === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: (fixer) => {
|
|
223
|
+
const bodyText = sourceCode.getText(node.body);
|
|
224
|
+
|
|
225
|
+
const rangeArgs =
|
|
226
|
+
step === undefined
|
|
227
|
+
? `${begin}, ${end}`
|
|
228
|
+
: `${begin}, ${end}, ${step}`;
|
|
229
|
+
|
|
230
|
+
const replacement = `for (const ${varName} of ${range.localName}(${rangeArgs})) ${bodyText}`;
|
|
231
|
+
|
|
232
|
+
return [
|
|
233
|
+
...range.edits(fixer),
|
|
234
|
+
fixer.replaceText(node, replacement),
|
|
235
|
+
];
|
|
236
|
+
},
|
|
261
237
|
});
|
|
262
238
|
}
|
|
263
239
|
},
|
|
@@ -286,6 +262,14 @@ const isNonIntegerNumberLiteral = (
|
|
|
286
262
|
);
|
|
287
263
|
};
|
|
288
264
|
|
|
265
|
+
const isWithin = (
|
|
266
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
267
|
+
node: TSESTree.Node,
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
269
|
+
container: TSESTree.Node,
|
|
270
|
+
): boolean =>
|
|
271
|
+
container.range[0] <= node.range[0] && node.range[1] <= container.range[1];
|
|
272
|
+
|
|
289
273
|
/**
|
|
290
274
|
* Whether the loop variable declared by `declaration` is written anywhere but
|
|
291
275
|
* its initializer and the loop's `update`.
|
|
@@ -307,11 +291,3 @@ const isAssignedOutsideUpdate = (
|
|
|
307
291
|
reference.init !== true &&
|
|
308
292
|
!isWithin(reference.identifier, update),
|
|
309
293
|
);
|
|
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];
|
|
@@ -309,8 +309,8 @@ describe('prefer-range-for-loop where the rewrite would change behavior', () =>
|
|
|
309
309
|
errors: [{ messageId: 'useRangeForLoop' }],
|
|
310
310
|
},
|
|
311
311
|
{
|
|
312
|
-
// The type checker judges it: a `number` step makes the fixed call
|
|
313
|
-
//
|
|
312
|
+
// The type checker judges it: a `number` step makes the fixed call fail
|
|
313
|
+
// to type-check.
|
|
314
314
|
name: 'a variable step is fixed without asking its type',
|
|
315
315
|
code: dedent`
|
|
316
316
|
declare const n: number;
|