eslint-plugin-ts-data-forge 0.8.6 → 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 -2
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +51 -4
- 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/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-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +2 -149
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
- package/dist/rules/prefer-canonical-array-slicing.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 +110 -40
- 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 +102 -45
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +97 -16
- 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 -11
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +72 -5
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- 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-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-uniq.mts +2 -207
- package/src/rules/prefer-canonical-array-slicing.mts +57 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-is-non-null-object.mts +160 -57
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +148 -67
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-range-for-loop.mts +142 -21
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
|
@@ -188,6 +188,25 @@ describe('prefer-arr-is-bounded-length-array through type wrappers', () => {
|
|
|
188
188
|
{ messageId: 'useIsBoundedLengthArray' },
|
|
189
189
|
],
|
|
190
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: 'either comparison wrapped as a whole',
|
|
193
|
+
code: dedent`
|
|
194
|
+
import { Arr } from 'ts-data-forge';
|
|
195
|
+
declare const xs: readonly number[];
|
|
196
|
+
const a = (xs.length >= 1 satisfies boolean) && xs.length <= 3;
|
|
197
|
+
const b = xs.length >= 1 && (xs.length <= 3 as boolean);
|
|
198
|
+
`,
|
|
199
|
+
output: dedent`
|
|
200
|
+
import { Arr } from 'ts-data-forge';
|
|
201
|
+
declare const xs: readonly number[];
|
|
202
|
+
const a = Arr.isBoundedLengthArray(1, 3, xs);
|
|
203
|
+
const b = Arr.isBoundedLengthArray(1, 3, xs);
|
|
204
|
+
`,
|
|
205
|
+
errors: [
|
|
206
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
207
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
191
210
|
],
|
|
192
211
|
},
|
|
193
212
|
);
|
|
@@ -205,6 +205,14 @@ describe('prefer-arr-is-max-length-array through type wrappers', () => {
|
|
|
205
205
|
const ok = xs!.length >= 1 && xs.length <= 3;
|
|
206
206
|
`,
|
|
207
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: 'one half of a bounded pair, either half wrapped as a whole',
|
|
210
|
+
code: dedent`
|
|
211
|
+
declare const xs: readonly number[];
|
|
212
|
+
const a = (xs.length >= 1 satisfies boolean) && xs.length <= 3;
|
|
213
|
+
const b = xs.length >= 1 && (xs.length <= 3 as boolean);
|
|
214
|
+
`,
|
|
215
|
+
},
|
|
208
216
|
],
|
|
209
217
|
invalid: [
|
|
210
218
|
{
|
|
@@ -219,6 +219,14 @@ describe('prefer-arr-is-min-length-array through type wrappers', () => {
|
|
|
219
219
|
const ok = xs.length >= 1 && (xs as readonly number[]).length <= 3;
|
|
220
220
|
`,
|
|
221
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'one half of a bounded pair, either half wrapped as a whole',
|
|
224
|
+
code: dedent`
|
|
225
|
+
declare const xs: readonly number[];
|
|
226
|
+
const a = (xs.length >= 1 satisfies boolean) && xs.length <= 3;
|
|
227
|
+
const b = xs.length >= 1 && (xs.length <= 3 as boolean);
|
|
228
|
+
`,
|
|
229
|
+
},
|
|
222
230
|
],
|
|
223
231
|
invalid: [
|
|
224
232
|
{
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
} from 'eslint-rule-forge';
|
|
15
15
|
import { Arr } from 'ts-data-forge';
|
|
16
16
|
import * as ts from 'typescript';
|
|
17
|
-
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
17
|
+
import { primitiveTypeFlags, TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
18
|
+
import { needsMutableResult } from './mutable-result.mjs';
|
|
18
19
|
|
|
19
20
|
type Options = readonly [];
|
|
20
21
|
|
|
@@ -215,19 +216,6 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
215
216
|
defaultOptions: [],
|
|
216
217
|
} as const;
|
|
217
218
|
|
|
218
|
-
/** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
|
|
219
|
-
const mutatingMethods: ReadonlySet<string> = new Set([
|
|
220
|
-
'copyWithin',
|
|
221
|
-
'fill',
|
|
222
|
-
'pop',
|
|
223
|
-
'push',
|
|
224
|
-
'reverse',
|
|
225
|
-
'shift',
|
|
226
|
-
'sort',
|
|
227
|
-
'splice',
|
|
228
|
-
'unshift',
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
219
|
/**
|
|
232
220
|
* Returns `xs` for `new Set(xs)`, and `undefined` for anything else — including
|
|
233
221
|
* `new Set()`, `new Set<T>(xs)` (whose element type may be wider than `xs`'s)
|
|
@@ -317,15 +305,6 @@ const isPrimitiveType = (
|
|
|
317
305
|
: (resolved.flags & primitiveTypeFlags) !== 0;
|
|
318
306
|
};
|
|
319
307
|
|
|
320
|
-
const primitiveTypeFlags =
|
|
321
|
-
ts.TypeFlags.StringLike |
|
|
322
|
-
ts.TypeFlags.NumberLike |
|
|
323
|
-
ts.TypeFlags.BigIntLike |
|
|
324
|
-
ts.TypeFlags.BooleanLike |
|
|
325
|
-
ts.TypeFlags.ESSymbolLike |
|
|
326
|
-
ts.TypeFlags.Null |
|
|
327
|
-
ts.TypeFlags.Undefined;
|
|
328
|
-
|
|
329
308
|
const resolveConstraint = (
|
|
330
309
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
331
310
|
checker: ts.TypeChecker,
|
|
@@ -335,187 +314,3 @@ const resolveConstraint = (
|
|
|
335
314
|
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
336
315
|
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
337
316
|
: type;
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Whether the code around `node` visibly relies on the result being a mutable
|
|
341
|
-
* array, so that swapping in the readonly array `Arr.uniq` returns would not
|
|
342
|
-
* compile. Looks at `node` itself and, when it initializes a variable without a
|
|
343
|
-
* type annotation, at every read of that variable.
|
|
344
|
-
*/
|
|
345
|
-
const needsMutableResult = (
|
|
346
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
347
|
-
sourceCode: TSESLint.SourceCode,
|
|
348
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
349
|
-
checker: ts.TypeChecker,
|
|
350
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
351
|
-
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
352
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
353
|
-
node: TSESTree.Node,
|
|
354
|
-
): boolean => {
|
|
355
|
-
const outer = outermostTypePreservingWrapper(node);
|
|
356
|
-
|
|
357
|
-
if (isUsedAsMutable(checker, getTsNode, outer)) {
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const parent = outer.parent;
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
|
|
365
|
-
parent.init !== outer ||
|
|
366
|
-
parent.id.type !== AST_NODE_TYPES.Identifier ||
|
|
367
|
-
parent.id.typeAnnotation !== undefined
|
|
368
|
-
) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const [variable] = sourceCode.getDeclaredVariables(parent);
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
variable?.references.some(
|
|
376
|
-
(reference) =>
|
|
377
|
-
reference.init !== true &&
|
|
378
|
-
isUsedAsMutable(
|
|
379
|
-
checker,
|
|
380
|
-
getTsNode,
|
|
381
|
-
outermostTypePreservingWrapper(reference.identifier),
|
|
382
|
-
),
|
|
383
|
-
) ?? false
|
|
384
|
-
);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* `node`, or the outermost `!` / `satisfies` around it. Neither changes the
|
|
389
|
-
* type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
|
|
390
|
-
* `ys!.push(4)` still need a mutable array. An `as` does change it, and a
|
|
391
|
-
* readonly array may be cast to a mutable one, so the walk stops there.
|
|
392
|
-
*/
|
|
393
|
-
const outermostTypePreservingWrapper = (
|
|
394
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
395
|
-
node: TSESTree.Node,
|
|
396
|
-
): TSESTree.Node => {
|
|
397
|
-
const { parent } = node;
|
|
398
|
-
|
|
399
|
-
return parent !== undefined &&
|
|
400
|
-
(parent.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
401
|
-
parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
|
|
402
|
-
parent.expression === node
|
|
403
|
-
? outermostTypePreservingWrapper(parent)
|
|
404
|
-
: node;
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const isUsedAsMutable = (
|
|
408
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
409
|
-
checker: ts.TypeChecker,
|
|
410
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
411
|
-
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
412
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
413
|
-
node: TSESTree.Node,
|
|
414
|
-
): boolean => {
|
|
415
|
-
const parent = node.parent;
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
parent?.type === AST_NODE_TYPES.MemberExpression &&
|
|
419
|
-
parent.object === node
|
|
420
|
-
) {
|
|
421
|
-
// xs.push(...), xs.sort(), ...
|
|
422
|
-
if (
|
|
423
|
-
!parent.computed &&
|
|
424
|
-
parent.property.type === AST_NODE_TYPES.Identifier &&
|
|
425
|
-
mutatingMethods.has(parent.property.name)
|
|
426
|
-
) {
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
|
|
431
|
-
const grandParent = parent.parent;
|
|
432
|
-
|
|
433
|
-
return (
|
|
434
|
-
(grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
435
|
-
grandParent.left === parent) ||
|
|
436
|
-
grandParent.type === AST_NODE_TYPES.UpdateExpression ||
|
|
437
|
-
(grandParent.type === AST_NODE_TYPES.UnaryExpression &&
|
|
438
|
-
grandParent.operator === 'delete')
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Passed to a parameter, assigned to a variable, or returned from a function
|
|
443
|
-
// whose declared type is a mutable array.
|
|
444
|
-
const tsNode = getTsNode(node);
|
|
445
|
-
|
|
446
|
-
if (tsNode === undefined) {
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (!ts.isExpression(tsNode)) {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const expectedType =
|
|
455
|
-
getDeclaredParameterType(checker, tsNode) ??
|
|
456
|
-
checker.getContextualType(tsNode);
|
|
457
|
-
|
|
458
|
-
return (
|
|
459
|
-
expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
|
|
460
|
-
);
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* The declared type of the parameter `argument` is passed to. The contextual
|
|
465
|
-
* type of an argument to a generic function is instantiated from that very
|
|
466
|
-
* argument — `Arr.toSorted(Array.from(set), cmp)` has `Ar` inferred as
|
|
467
|
-
* `string[]` — so it would read as "a mutable array is required" for any
|
|
468
|
-
* generic parameter. The declared type (`Ar extends readonly unknown[]`) is
|
|
469
|
-
* what the rewritten argument actually has to satisfy.
|
|
470
|
-
*/
|
|
471
|
-
const getDeclaredParameterType = (
|
|
472
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
473
|
-
checker: ts.TypeChecker,
|
|
474
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
475
|
-
argument: ts.Expression,
|
|
476
|
-
): ts.Type | undefined => {
|
|
477
|
-
const call = argument.parent;
|
|
478
|
-
|
|
479
|
-
if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) {
|
|
480
|
-
return undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const index = call.arguments?.indexOf(argument) ?? -1;
|
|
484
|
-
|
|
485
|
-
if (index === -1) {
|
|
486
|
-
return undefined;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const declaration = checker.getResolvedSignature(call)?.getDeclaration();
|
|
490
|
-
|
|
491
|
-
const parameter = declaration?.parameters[index];
|
|
492
|
-
|
|
493
|
-
// A rest parameter's declared type is the array of arguments, not the type of
|
|
494
|
-
// one; the contextual type is the better answer there.
|
|
495
|
-
return parameter === undefined || parameter.dotDotDotToken !== undefined
|
|
496
|
-
? undefined
|
|
497
|
-
: checker.getTypeAtLocation(parameter);
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Whether a readonly array cannot be assigned to `type`: it is a mutable array,
|
|
502
|
-
* or a union of mutable arrays and primitives. (A tuple accepted no array to
|
|
503
|
-
* begin with, so it does not matter here.)
|
|
504
|
-
*/
|
|
505
|
-
const isMutableArrayOnly = (
|
|
506
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
507
|
-
checker: ts.TypeChecker,
|
|
508
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
509
|
-
type: ts.Type,
|
|
510
|
-
): boolean => {
|
|
511
|
-
const members = type.isUnion() ? type.types : ([type] as const);
|
|
512
|
-
|
|
513
|
-
return (
|
|
514
|
-
members.every(
|
|
515
|
-
(t) =>
|
|
516
|
-
// `null`, `undefined` and other primitives accept no array at all.
|
|
517
|
-
(t.flags & primitiveTypeFlags) !== 0 ||
|
|
518
|
-
(checker.isArrayType(t) && t.getSymbol()?.getName() === 'Array'),
|
|
519
|
-
) && members.some((t) => (t.flags & primitiveTypeFlags) === 0)
|
|
520
|
-
);
|
|
521
|
-
};
|
|
@@ -84,23 +84,32 @@ const asNegatedPositiveIntegerLiteral = (
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Whether re-reading the expression is free of side effects (a plain variable
|
|
87
|
-
* or a non-computed property chain). Required for
|
|
88
|
-
* 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)`.
|
|
89
90
|
*/
|
|
90
|
-
const isSimpleReference = (
|
|
91
|
-
node
|
|
92
|
-
|
|
93
|
-
(
|
|
94
|
-
|
|
95
|
-
node.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
100
108
|
? node.name
|
|
101
109
|
: node.type === AST_NODE_TYPES.MemberExpression
|
|
102
110
|
? rootIdentifierName(node.object)
|
|
103
111
|
: undefined;
|
|
112
|
+
};
|
|
104
113
|
|
|
105
114
|
type ComparisonOperator = '!==' | '<' | '<=' | '>' | '>=';
|
|
106
115
|
|
|
@@ -338,26 +347,41 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
338
347
|
});
|
|
339
348
|
};
|
|
340
349
|
|
|
341
|
-
/**
|
|
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
|
+
*/
|
|
342
355
|
const isReceiverLengthAccess = (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
): boolean =>
|
|
346
|
-
node
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
};
|
|
351
370
|
|
|
352
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Matches `<receiver>.length - M` (M ≥ 1), under any type wrapper, and
|
|
373
|
+
* returns `M`.
|
|
374
|
+
*/
|
|
353
375
|
const matchLengthMinus = (
|
|
354
|
-
|
|
355
|
-
|
|
376
|
+
wrapped: TSESTree.Node,
|
|
377
|
+
receiver: TSESTree.Expression,
|
|
356
378
|
): number | undefined => {
|
|
379
|
+
const node = skipTypeWrappers(wrapped);
|
|
380
|
+
|
|
357
381
|
if (
|
|
358
382
|
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
359
383
|
node.operator !== '-' ||
|
|
360
|
-
!isReceiverLengthAccess(node.left,
|
|
384
|
+
!isReceiverLengthAccess(node.left, receiver)
|
|
361
385
|
) {
|
|
362
386
|
return undefined;
|
|
363
387
|
}
|
|
@@ -375,7 +399,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
375
399
|
const classifyFilterCallback = (
|
|
376
400
|
callback: TSESTree.CallExpressionArgument,
|
|
377
401
|
receiver: TSESTree.Expression,
|
|
378
|
-
receiverText: string,
|
|
379
402
|
): CopyOp | undefined => {
|
|
380
403
|
if (
|
|
381
404
|
callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
@@ -409,7 +432,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
409
432
|
return undefined;
|
|
410
433
|
}
|
|
411
434
|
|
|
412
|
-
|
|
435
|
+
// `(i >= 1) satisfies boolean` is still the comparison.
|
|
436
|
+
const body = skipTypeWrappers(callback.body);
|
|
413
437
|
|
|
414
438
|
if (
|
|
415
439
|
body.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
@@ -456,7 +480,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
456
480
|
return undefined;
|
|
457
481
|
}
|
|
458
482
|
|
|
459
|
-
const m = matchLengthMinus(other,
|
|
483
|
+
const m = matchLengthMinus(other, receiver);
|
|
460
484
|
|
|
461
485
|
return m === undefined ? undefined : classifyLengthComparison(op, m);
|
|
462
486
|
};
|
|
@@ -512,7 +536,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
512
536
|
return undefined;
|
|
513
537
|
}
|
|
514
538
|
|
|
515
|
-
const m = matchLengthMinus(end,
|
|
539
|
+
const m = matchLengthMinus(end, receiver);
|
|
516
540
|
|
|
517
541
|
return m === undefined ? undefined : normalizeCountedOp('skipLast', m);
|
|
518
542
|
}
|
|
@@ -523,7 +547,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
523
547
|
const classifyToSplicedArgs = (
|
|
524
548
|
args: readonly TSESTree.CallExpressionArgument[],
|
|
525
549
|
receiver: TSESTree.Expression,
|
|
526
|
-
receiverText: string,
|
|
527
550
|
): CopyOp | undefined => {
|
|
528
551
|
if (args.length === 1) {
|
|
529
552
|
const [start] = args;
|
|
@@ -572,7 +595,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
572
595
|
return undefined;
|
|
573
596
|
}
|
|
574
597
|
|
|
575
|
-
const m = matchLengthMinus(deleteCount,
|
|
598
|
+
const m = matchLengthMinus(deleteCount, receiver);
|
|
576
599
|
|
|
577
600
|
return m === undefined ? undefined : normalizeCountedOp('takeLast', m);
|
|
578
601
|
}
|
|
@@ -598,7 +621,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
598
621
|
// `xs.toSpliced(xs.length, 0, item)` appends → toPushed.
|
|
599
622
|
if (
|
|
600
623
|
isSimpleReference(receiver) &&
|
|
601
|
-
isReceiverLengthAccess(start,
|
|
624
|
+
isReceiverLengthAccess(start, receiver)
|
|
602
625
|
) {
|
|
603
626
|
return { fn: 'toPushed', itemText: toArgText(item) };
|
|
604
627
|
}
|
|
@@ -702,8 +725,6 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
702
725
|
return;
|
|
703
726
|
}
|
|
704
727
|
|
|
705
|
-
const receiverText = sourceCode.getText(receiver);
|
|
706
|
-
|
|
707
728
|
switch (methodName) {
|
|
708
729
|
case 'slice': {
|
|
709
730
|
const op = classifySliceArgs(node.arguments, receiver);
|
|
@@ -716,11 +737,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
716
737
|
}
|
|
717
738
|
|
|
718
739
|
case 'toSpliced': {
|
|
719
|
-
const op = classifyToSplicedArgs(
|
|
720
|
-
node.arguments,
|
|
721
|
-
receiver,
|
|
722
|
-
receiverText,
|
|
723
|
-
);
|
|
740
|
+
const op = classifyToSplicedArgs(node.arguments, receiver);
|
|
724
741
|
|
|
725
742
|
if (op !== undefined) {
|
|
726
743
|
report(node, op, toArgText(receiver));
|
|
@@ -736,7 +753,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
|
|
|
736
753
|
return;
|
|
737
754
|
}
|
|
738
755
|
|
|
739
|
-
const op = classifyFilterCallback(callback, receiver
|
|
756
|
+
const op = classifyFilterCallback(callback, receiver);
|
|
740
757
|
|
|
741
758
|
if (op !== undefined) {
|
|
742
759
|
report(node, op, toArgText(receiver));
|
|
@@ -675,6 +675,61 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
|
|
|
675
675
|
{ messageId: 'preferCanonicalArraySlicing' },
|
|
676
676
|
],
|
|
677
677
|
},
|
|
678
|
+
{
|
|
679
|
+
name: 'a filter callback whose body is wrapped',
|
|
680
|
+
code: dedent`
|
|
681
|
+
declare const xs: readonly number[];
|
|
682
|
+
const ys = xs.filter((_, i) => (i >= 1) satisfies boolean);
|
|
683
|
+
`,
|
|
684
|
+
output: dedent`
|
|
685
|
+
import { Arr } from 'ts-data-forge';
|
|
686
|
+
declare const xs: readonly number[];
|
|
687
|
+
const ys = Arr.tail(xs);
|
|
688
|
+
`,
|
|
689
|
+
errors: [{ messageId: 'preferCanonicalArraySlicing' }],
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: 'a wrapped `xs.length - N`, and a wrapped `.length` or receiver inside it',
|
|
693
|
+
code: dedent`
|
|
694
|
+
declare const xs: readonly number[];
|
|
695
|
+
const a = xs.slice(0, (xs.length - 1) as number);
|
|
696
|
+
const b = xs.slice(0, (xs as readonly number[]).length - 1);
|
|
697
|
+
const c = xs.filter((_, i) => i < (xs.length satisfies number) - 2);
|
|
698
|
+
const d = xs.toSpliced(0, xs!.length - 2);
|
|
699
|
+
`,
|
|
700
|
+
output: dedent`
|
|
701
|
+
import { Arr } from 'ts-data-forge';
|
|
702
|
+
declare const xs: readonly number[];
|
|
703
|
+
const a = Arr.butLast(xs);
|
|
704
|
+
const b = Arr.butLast(xs);
|
|
705
|
+
const c = Arr.skipLast(xs, 2);
|
|
706
|
+
const d = Arr.takeLast(xs, 2);
|
|
707
|
+
`,
|
|
708
|
+
errors: [
|
|
709
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
710
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
711
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
712
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
713
|
+
],
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'a wrapped receiver compared with its own `.length`',
|
|
717
|
+
code: dedent`
|
|
718
|
+
declare const xs: readonly number[];
|
|
719
|
+
const a = (xs satisfies readonly number[]).slice(0, xs.length - 1);
|
|
720
|
+
const b = xs.toSpliced((xs.length as number), 0, 1);
|
|
721
|
+
`,
|
|
722
|
+
output: dedent`
|
|
723
|
+
import { Arr } from 'ts-data-forge';
|
|
724
|
+
declare const xs: readonly number[];
|
|
725
|
+
const a = Arr.butLast(xs satisfies readonly number[]);
|
|
726
|
+
const b = Arr.toPushed(xs, 1);
|
|
727
|
+
`,
|
|
728
|
+
errors: [
|
|
729
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
730
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
731
|
+
],
|
|
732
|
+
},
|
|
678
733
|
],
|
|
679
734
|
});
|
|
680
735
|
});
|