eslint-plugin-ts-data-forge 0.8.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +21 -19
  2. package/dist/plugin.d.mts +4 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +2 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +31 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -2
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +51 -4
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  22. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  23. package/dist/rules/prefer-arr-generate.mjs +588 -0
  24. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  25. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-uniq.mjs +2 -149
  27. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  28. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  29. package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
  30. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  31. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-non-null-object.mjs +110 -40
  33. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  34. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  35. package/dist/rules/prefer-is-record-and-has-key.mjs +102 -45
  36. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  37. package/dist/rules/prefer-pipe-map-nullable.d.mts +24 -0
  38. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -0
  39. package/dist/rules/prefer-pipe-map-nullable.mjs +266 -0
  40. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -0
  41. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  42. package/dist/rules/prefer-range-for-loop.mjs +97 -16
  43. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  44. package/dist/rules/rules.d.mts +2 -0
  45. package/dist/rules/rules.d.mts.map +1 -1
  46. package/dist/rules/rules.mjs +4 -0
  47. package/dist/rules/rules.mjs.map +1 -1
  48. package/package.json +11 -11
  49. package/src/plugin.mts +2 -0
  50. package/src/rule-types.mts +33 -1
  51. package/src/rules/ast-utils.mts +72 -5
  52. package/src/rules/constants.mts +12 -0
  53. package/src/rules/mutable-result.mts +204 -0
  54. package/src/rules/prefer-arr-generate.mts +918 -0
  55. package/src/rules/prefer-arr-generate.test.mts +967 -0
  56. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  57. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  58. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  59. package/src/rules/prefer-arr-uniq.mts +2 -207
  60. package/src/rules/prefer-canonical-array-slicing.mts +57 -40
  61. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  62. package/src/rules/prefer-is-non-null-object.mts +160 -57
  63. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  64. package/src/rules/prefer-is-record-and-has-key.mts +148 -67
  65. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  66. package/src/rules/prefer-pipe-map-nullable.mts +391 -0
  67. package/src/rules/prefer-pipe-map-nullable.test.mts +470 -0
  68. package/src/rules/prefer-range-for-loop.mts +142 -21
  69. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  70. package/src/rules/rules.mts +4 -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 patterns that reference the
88
- * receiver twice, such as `xs.filter((_, i) => i < xs.length - 1)`.
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 = (node: TSESTree.Node): boolean =>
91
- node.type === AST_NODE_TYPES.Identifier ||
92
- node.type === AST_NODE_TYPES.ThisExpression ||
93
- (node.type === AST_NODE_TYPES.MemberExpression &&
94
- !node.computed &&
95
- node.property.type === AST_NODE_TYPES.Identifier &&
96
- isSimpleReference(node.object));
97
-
98
- const rootIdentifierName = (node: TSESTree.Node): string | undefined =>
99
- node.type === AST_NODE_TYPES.Identifier
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
- /** Matches `<xsText>.length` (non-computed). */
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
- node: TSESTree.Node,
344
- receiverText: string,
345
- ): boolean =>
346
- node.type === AST_NODE_TYPES.MemberExpression &&
347
- !node.computed &&
348
- node.property.type === AST_NODE_TYPES.Identifier &&
349
- node.property.name === 'length' &&
350
- sourceCode.getText(node.object) === receiverText;
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
- /** Matches `<xsText>.length - M` (M ≥ 1) and returns `M`. */
371
+ /**
372
+ * Matches `<receiver>.length - M` (M ≥ 1), under any type wrapper, and
373
+ * returns `M`.
374
+ */
353
375
  const matchLengthMinus = (
354
- node: TSESTree.Node,
355
- receiverText: string,
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, receiverText)
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
- const body = callback.body;
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, receiverText);
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, sourceCode.getText(receiver));
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, receiverText);
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, receiverText)
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, receiverText);
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
  });