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.
- package/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- 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 +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -34
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +53 -58
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +7 -5
- package/dist/rules/prefer-arr-is-array.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 +9 -6
- 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 +9 -6
- 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 +9 -6
- 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 +9 -6
- 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 +9 -6
- 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 +8 -11
- 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 +8 -6
- 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 +12 -166
- 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 +6 -8
- 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 +57 -34
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +3 -3
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +4 -4
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs +3 -2
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +113 -43
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +105 -48
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +6 -6
- package/dist/rules/prefer-num-safe-parse-int.mjs.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 +9 -12
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +104 -23
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -10
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +73 -80
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/no-unnecessary-type-guard.mts +14 -10
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-array.mts +14 -7
- package/src/rules/prefer-arr-is-array.test.mts +14 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -13
- package/src/rules/prefer-arr-scan.mts +24 -19
- package/src/rules/prefer-arr-sum.mts +18 -14
- package/src/rules/prefer-arr-uniq.mts +25 -234
- package/src/rules/prefer-as-int.mts +19 -12
- package/src/rules/prefer-canonical-array-slicing.mts +79 -52
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-canonical-length-cast.mts +7 -3
- package/src/rules/prefer-canonical-length-guard.mts +9 -4
- package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
- package/src/rules/prefer-is-non-null-object.mts +171 -64
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +159 -74
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-num-safe-parse-float.mts +15 -10
- package/src/rules/prefer-num-safe-parse-int.mts +15 -13
- package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
- package/src/rules/prefer-range-for-loop.mts +157 -31
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
- package/dist/rules/import-utils.d.mts +0 -30
- package/dist/rules/import-utils.d.mts.map +0 -1
- package/dist/rules/import-utils.mjs +0 -98
- package/dist/rules/import-utils.mjs.map +0 -1
- 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
|
-
|
|
8
|
+
buildNamedImportFix,
|
|
9
|
+
getArgumentText,
|
|
10
|
+
getImportDeclarations,
|
|
11
11
|
getImportedLocalName,
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
?
|
|
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 ===
|
|
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
|
-
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getImportDeclarations,
|
|
11
10
|
getNamedImports,
|
|
12
|
-
|
|
13
|
-
} from '
|
|
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
|
|
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(
|
|
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}(${
|
|
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
|
-
?
|
|
96
|
-
|
|
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
|
-
|
|
8
|
+
buildNamedImportFix,
|
|
9
|
+
getImportDeclarations,
|
|
12
10
|
getImportedLocalName,
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
86
|
-
* receiver twice, such as
|
|
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 = (
|
|
89
|
-
node
|
|
90
|
-
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
node.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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(
|
|
216
|
+
const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
|
|
203
217
|
|
|
204
|
-
const namespaceLocalName =
|
|
205
|
-
(
|
|
206
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
): boolean =>
|
|
338
|
-
node
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Matches `<receiver>.length - M` (M ≥ 1), under any type wrapper, and
|
|
373
|
+
* returns `M`.
|
|
374
|
+
*/
|
|
345
375
|
const matchLengthMinus = (
|
|
346
|
-
|
|
347
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
?
|
|
865
|
+
? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
866
|
+
'Arr',
|
|
867
|
+
])
|
|
841
868
|
: [];
|
|
842
869
|
|
|
843
870
|
return [
|