eslint-plugin-ts-data-forge 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/index.d.mts +1 -1
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/plugin.d.mts +1 -1
  4. package/dist/plugin.d.mts.map +1 -1
  5. package/dist/plugin.mjs +1 -0
  6. package/dist/plugin.mjs.map +1 -1
  7. package/dist/rule-types.d.mts +3 -3
  8. package/dist/rule-types.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +59 -3
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +119 -8
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/import-utils.d.mts +1 -1
  14. package/dist/rules/import-utils.d.mts.map +1 -1
  15. package/dist/rules/import-utils.mjs +22 -11
  16. package/dist/rules/import-utils.mjs.map +1 -1
  17. package/dist/rules/no-side-effect-import.d.mts +1 -1
  18. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  19. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  20. package/dist/rules/no-unnecessary-type-guard.mjs +125 -79
  21. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  22. package/dist/rules/prefer-arr-is-array.mjs +11 -4
  23. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +5 -19
  27. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
  29. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +11 -27
  31. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
  33. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -22
  35. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
  37. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -22
  39. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-non-empty.mjs +7 -22
  42. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-scan.d.mts +6 -5
  44. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  45. package/dist/rules/prefer-arr-scan.mjs +33 -13
  46. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  47. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  48. package/dist/rules/prefer-arr-sum.mjs +51 -37
  49. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  50. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  51. package/dist/rules/prefer-arr-uniq.mjs +33 -10
  52. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  53. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  54. package/dist/rules/prefer-as-int.mjs +18 -12
  55. package/dist/rules/prefer-as-int.mjs.map +1 -1
  56. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  57. package/dist/rules/prefer-canonical-array-slicing.mjs +29 -14
  58. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  59. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  60. package/dist/rules/prefer-canonical-length-cast.mjs +34 -13
  61. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  62. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  63. package/dist/rules/prefer-canonical-length-guard.mjs +10 -4
  64. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  65. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  66. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +25 -3
  67. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  68. package/dist/rules/prefer-is-non-null-object.mjs +28 -10
  69. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  70. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  71. package/dist/rules/prefer-is-record-and-has-key.mjs +8 -3
  72. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  73. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  74. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  75. package/dist/rules/prefer-num-safe-parse-float.mjs +18 -7
  76. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  77. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  78. package/dist/rules/prefer-num-safe-parse-int.mjs +15 -8
  79. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  80. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  81. package/dist/rules/prefer-range-for-loop.mjs +13 -7
  82. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  83. package/dist/types.d.mts +2 -2
  84. package/dist/types.d.mts.map +1 -1
  85. package/package.json +7 -7
  86. package/src/index.mts +1 -1
  87. package/src/plugin.mts +5 -4
  88. package/src/plugin.test.mts +0 -13
  89. package/src/rule-types.mts +3 -3
  90. package/src/rules/ast-utils.mts +156 -13
  91. package/src/rules/import-utils.mts +35 -15
  92. package/src/rules/no-side-effect-import.mts +1 -1
  93. package/src/rules/no-unnecessary-type-guard.mts +192 -124
  94. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  95. package/src/rules/prefer-arr-is-array.mts +11 -6
  96. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  97. package/src/rules/prefer-arr-is-bounded-length-array.mts +13 -25
  98. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  99. package/src/rules/prefer-arr-is-fixed-length-array.mts +18 -38
  100. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  101. package/src/rules/prefer-arr-is-max-length-array.mts +15 -26
  102. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  103. package/src/rules/prefer-arr-is-min-length-array.mts +15 -26
  104. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  105. package/src/rules/prefer-arr-is-non-empty.mts +16 -25
  106. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  107. package/src/rules/prefer-arr-scan.mts +55 -19
  108. package/src/rules/prefer-arr-scan.test.mts +149 -0
  109. package/src/rules/prefer-arr-sum.mts +78 -47
  110. package/src/rules/prefer-arr-sum.test.mts +97 -0
  111. package/src/rules/prefer-arr-uniq.mts +46 -12
  112. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  113. package/src/rules/prefer-as-int.mts +24 -16
  114. package/src/rules/prefer-as-int.test.mts +51 -0
  115. package/src/rules/prefer-canonical-array-slicing.mts +38 -15
  116. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  117. package/src/rules/prefer-canonical-length-cast.mts +45 -15
  118. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  119. package/src/rules/prefer-canonical-length-guard.mts +13 -7
  120. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  121. package/src/rules/prefer-comparison-over-nullish-guard.mts +33 -6
  122. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  123. package/src/rules/prefer-is-non-null-object.mts +31 -11
  124. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  125. package/src/rules/prefer-is-record-and-has-key.mts +10 -5
  126. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  127. package/src/rules/prefer-num-safe-parse-float.mts +24 -11
  128. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  129. package/src/rules/prefer-num-safe-parse-int.mts +25 -11
  130. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  131. package/src/rules/prefer-obj-over-entries-round-trip.mts +1 -1
  132. package/src/rules/prefer-range-for-loop.mts +18 -8
  133. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  134. package/src/rules/rules.mts +1 -1
  135. package/src/types.mts +2 -2
@@ -460,3 +460,103 @@ describe('no-unnecessary-type-guard', () => {
460
460
  ],
461
461
  });
462
462
  });
463
+
464
+ describe('no-unnecessary-type-guard through type wrappers', () => {
465
+ tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
466
+ valid: [
467
+ {
468
+ name: 'isNullish on a value cast away from null',
469
+ code: dedent`
470
+ import { isNullish } from 'ts-data-forge';
471
+ declare const x: string | null | undefined;
472
+ const y = isNullish(x as string | undefined);
473
+ `,
474
+ },
475
+ {
476
+ name: 'isNonNullish on a value cast away from null',
477
+ code: dedent`
478
+ import { isNonNullish } from 'ts-data-forge';
479
+ declare const x: string | null | undefined;
480
+ const y = isNonNullish(x as string | undefined);
481
+ `,
482
+ },
483
+ {
484
+ name: 'isNonEmptyString on a string cast to NonEmptyString',
485
+ code: dedent`
486
+ import { type NonEmptyString } from 'ts-type-forge';
487
+ import { isNonEmptyString } from 'ts-data-forge';
488
+ declare const x: string | null | undefined;
489
+ const y = isNonEmptyString(x as NonEmptyString | null | undefined);
490
+ `,
491
+ },
492
+ {
493
+ name: 'isNull on a value whose `!` does not remove the null',
494
+ code: dedent`
495
+ import { isNull } from 'ts-data-forge';
496
+ declare const x: string | null;
497
+ const y = isNull(x!);
498
+ `,
499
+ },
500
+ {
501
+ name: 'isNull on a value widened to include null: the `as` is the author saying it may be null',
502
+ code: dedent`
503
+ import { isNull } from 'ts-data-forge';
504
+ declare const x: string;
505
+ const y = isNull(x as string | null);
506
+ `,
507
+ },
508
+ {
509
+ name: 'isNullish on a value widened to include null is not narrowed to isUndefined',
510
+ code: dedent`
511
+ import { isNullish } from 'ts-data-forge';
512
+ declare const x: string | undefined;
513
+ const y = isNullish(x as string | null | undefined);
514
+ `,
515
+ },
516
+ ],
517
+ invalid: [
518
+ {
519
+ name: 'isNonNullish on a value whose `!` does not remove the null',
520
+ code: dedent`
521
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
522
+ declare const x: string | null;
523
+ const y = isNonNullish(x!);
524
+ `,
525
+ output: dedent`
526
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
527
+ declare const x: string | null;
528
+ const y = isNotNull(x!);
529
+ `,
530
+ errors: [{ messageId: 'replaceTypeGuard' }],
531
+ },
532
+ {
533
+ name: 'isNull on a value widened to exclude null still, at every layer',
534
+ code: dedent`
535
+ import { isNull } from 'ts-data-forge';
536
+ declare const x: string;
537
+ const y = isNull(x as string | undefined);
538
+ `,
539
+ output: dedent`
540
+ import { isNull } from 'ts-data-forge';
541
+ declare const x: string;
542
+ const y = false;
543
+ `,
544
+ errors: [{ messageId: 'alwaysFalse' }],
545
+ },
546
+ {
547
+ name: 'isNullish replaced on the value under `satisfies`, keeping the wrapper',
548
+ code: dedent`
549
+ import { isNullish, isUndefined } from 'ts-data-forge';
550
+ declare const x: string | undefined;
551
+ const y = isNullish(x satisfies string | null | undefined);
552
+ `,
553
+ output: dedent`
554
+ import { isNullish, isUndefined } from 'ts-data-forge';
555
+ declare const x: string | undefined;
556
+ const y = isUndefined(x satisfies string | null | undefined);
557
+ `,
558
+ errors: [{ messageId: 'replaceTypeGuard' }],
559
+ },
560
+ ],
561
+ });
562
+ });
@@ -3,7 +3,7 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { type DeepReadonly } from 'ts-type-forge';
6
+ import { skipTypeWrappers } from './ast-utils.mjs';
7
7
  import {
8
8
  buildImportFixes,
9
9
  getNamedImports,
@@ -65,15 +65,15 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
65
65
  return [];
66
66
  }
67
67
 
68
- const replacement = `Arr.isArray${sourceCode.getText(node).slice(sourceCode.getText(callee).length)}`;
69
-
68
+ // Only the callee is replaced: parentheses the source put around
69
+ // it are outside its range, and stay balanced.
70
70
  // Add import only for the first node and only if not already imported
71
71
  const importFixes =
72
72
  index === 0 && !hasArrImport
73
73
  ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
74
74
  : [];
75
75
 
76
- return [...importFixes, fixer.replaceText(node, replacement)];
76
+ return [...importFixes, fixer.replaceText(callee, 'Arr.isArray')];
77
77
  },
78
78
  });
79
79
  }
@@ -84,13 +84,18 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
84
84
  } as const;
85
85
 
86
86
  const isArrayIsArrayCall = (
87
- node: DeepReadonly<TSESTree.CallExpression>,
87
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
88
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
89
+ node: TSESTree.CallExpression,
88
90
  ): boolean => {
89
91
  if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
90
92
  return false;
91
93
  }
92
94
 
93
- const { object, property } = node.callee;
95
+ const { property } = node.callee;
96
+
97
+ // `(Array as ArrayConstructor).isArray` is still `Array.isArray`.
98
+ const object = skipTypeWrappers(node.callee.object);
94
99
 
95
100
  if (object.type !== AST_NODE_TYPES.Identifier) {
96
101
  return false;
@@ -60,3 +60,43 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
60
60
  },
61
61
  ],
62
62
  });
63
+
64
+ describe('prefer-arr-is-array through type wrappers', () => {
65
+ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
66
+ valid: [],
67
+ invalid: [
68
+ {
69
+ name: 'Array reached through `as`',
70
+ code: dedent`
71
+ import { Arr } from 'ts-data-forge';
72
+ const result = (Array as ArrayConstructor).isArray(value);
73
+ `,
74
+ output: dedent`
75
+ import { Arr } from 'ts-data-forge';
76
+ const result = Arr.isArray(value);
77
+ `,
78
+ errors: [{ messageId: 'useArrIsArray' }],
79
+ },
80
+ ],
81
+ });
82
+ });
83
+
84
+ describe('prefer-arr-is-array with parenthesized operands', () => {
85
+ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
86
+ valid: [],
87
+ invalid: [
88
+ {
89
+ name: 'a parenthesized callee',
90
+ code: dedent`
91
+ import { Arr } from 'ts-data-forge';
92
+ const result = (Array.isArray)(value);
93
+ `,
94
+ output: dedent`
95
+ import { Arr } from 'ts-data-forge';
96
+ const result = (Arr.isArray)(value);
97
+ `,
98
+ errors: [{ messageId: 'useArrIsArray' }],
99
+ },
100
+ ],
101
+ });
102
+ });
@@ -1,5 +1,10 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- import { parseLengthComparison } from './ast-utils.mjs';
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ import {
3
+ isArrayOrTupleExpression,
4
+ isSameArrayText,
5
+ parseLengthComparison,
6
+ toArgumentText,
7
+ } from './ast-utils.mjs';
3
8
  import {
4
9
  buildImportFixes,
5
10
  getNamedImports,
@@ -64,9 +69,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
64
69
  return;
65
70
  }
66
71
 
67
- if (
68
- sourceCode.getText(left.array) !== sourceCode.getText(right.array)
69
- ) {
72
+ if (!isSameArrayText(left.array, right.array, sourceCode)) {
70
73
  return;
71
74
  }
72
75
 
@@ -77,25 +80,10 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
77
80
  const arrayExpression = min.array;
78
81
 
79
82
  // Check if arrayExpression is actually an array type
80
- if (services?.program !== undefined && services.program !== null) {
81
- const checker = services.program.getTypeChecker();
82
-
83
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
84
-
85
- if (tsNode !== undefined) {
86
- const type = checker.getTypeAtLocation(tsNode);
87
-
88
- // Check if it's an array type or tuple type
89
- const isArrayType =
90
- checker.isArrayType(type) || checker.isTupleType(type);
91
-
92
- if (!isArrayType) {
93
- return;
94
- }
95
- } else {
96
- return;
97
- }
98
- } else {
83
+ if (
84
+ !isArrayOrTupleExpression(services, min.array) ||
85
+ !isArrayOrTupleExpression(services, max.array)
86
+ ) {
99
87
  return;
100
88
  }
101
89
 
@@ -115,7 +103,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
115
103
  index,
116
104
  { node, arrayExpression, minExpression, maxExpression },
117
105
  ] of mut_nodesToFix.entries()) {
118
- const arrayText = sourceCode.getText(arrayExpression);
106
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
119
107
 
120
108
  const minText = sourceCode.getText(minExpression);
121
109
 
@@ -152,3 +152,71 @@ describe('prefer-arr-is-bounded-length-array', () => {
152
152
  },
153
153
  );
154
154
  }, 20000);
155
+
156
+ describe('prefer-arr-is-bounded-length-array through type wrappers', () => {
157
+ tester.run(
158
+ 'prefer-arr-is-bounded-length-array',
159
+ preferArrIsBoundedLengthArray,
160
+ {
161
+ valid: [
162
+ {
163
+ name: 'a computed index named `length` is not the length',
164
+ code: dedent`
165
+ declare const xs: readonly number[];
166
+ declare const length: number;
167
+ const ok = xs[length] >= 1 && xs.length <= 3;
168
+ `,
169
+ },
170
+ ],
171
+ invalid: [
172
+ {
173
+ name: 'the array, the length and the bounds wrapped',
174
+ code: dedent`
175
+ import { Arr } from 'ts-data-forge';
176
+ declare const xs: readonly number[];
177
+ const a = xs.length >= 1 && (xs as readonly number[]).length <= 3;
178
+ const b = (xs.length satisfies number) >= (1 as number) && xs!.length <= 3;
179
+ `,
180
+ output: dedent`
181
+ import { Arr } from 'ts-data-forge';
182
+ declare const xs: readonly number[];
183
+ const a = Arr.isBoundedLengthArray(1, 3, xs);
184
+ const b = Arr.isBoundedLengthArray(1, 3, xs);
185
+ `,
186
+ errors: [
187
+ { messageId: 'useIsBoundedLengthArray' },
188
+ { messageId: 'useIsBoundedLengthArray' },
189
+ ],
190
+ },
191
+ ],
192
+ },
193
+ );
194
+ }, 20000);
195
+
196
+ describe('prefer-arr-is-bounded-length-array with parenthesized operands', () => {
197
+ tester.run(
198
+ 'prefer-arr-is-bounded-length-array',
199
+ preferArrIsBoundedLengthArray,
200
+ {
201
+ valid: [],
202
+ invalid: [
203
+ {
204
+ name: 'a sequence array keeps its parentheses',
205
+ code: dedent`
206
+ import { Arr } from 'ts-data-forge';
207
+ declare const xs: readonly number[];
208
+ declare const log: () => void;
209
+ const ok = (log(), xs).length >= 1 && (log(), xs).length <= 3;
210
+ `,
211
+ output: dedent`
212
+ import { Arr } from 'ts-data-forge';
213
+ declare const xs: readonly number[];
214
+ declare const log: () => void;
215
+ const ok = Arr.isBoundedLengthArray(1, 3, (log(), xs));
216
+ `,
217
+ errors: [{ messageId: 'useIsBoundedLengthArray' }],
218
+ },
219
+ ],
220
+ },
221
+ );
222
+ }, 20000);
@@ -1,10 +1,11 @@
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
1
2
  import {
2
- AST_NODE_TYPES,
3
- type TSESLint,
4
- type TSESTree,
5
- } from '@typescript-eslint/utils';
6
- import { type DeepReadonly } from 'ts-type-forge';
7
- import { isIntegerLiteralOrConstant } from './ast-utils.mjs';
3
+ asLengthAccess,
4
+ isArrayOrTupleExpression,
5
+ isIntegerLiteralOrConstant,
6
+ skipTypeWrappers,
7
+ toArgumentText,
8
+ } from './ast-utils.mjs';
8
9
  import {
9
10
  buildImportFixes,
10
11
  getNamedImports,
@@ -58,13 +59,16 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
58
59
 
59
60
  const isNegated = node.operator === '!==';
60
61
 
61
- const isLengthOnLeft = isLengthAccess(node.left);
62
+ // Both sides are read through type wrappers.
63
+ const leftLength = asLengthAccess(node.left);
62
64
 
63
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
65
+ const isLengthOnLeft = leftLength !== undefined;
66
+
67
+ const lengthSide = leftLength ?? asLengthAccess(node.right);
64
68
 
65
69
  const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
66
70
 
67
- if (!isLengthAccess(lengthSide)) {
71
+ if (lengthSide === undefined) {
68
72
  return;
69
73
  }
70
74
 
@@ -73,36 +77,19 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
73
77
  return;
74
78
  }
75
79
 
76
- // lengthSide is MemberExpression accessing .length
77
80
  const arrayExpression = lengthSide.object;
78
81
 
79
82
  // Check if arrayExpression is actually an array type
80
- if (services?.program !== undefined && services.program !== null) {
81
- const checker = services.program.getTypeChecker();
82
-
83
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
84
-
85
- if (tsNode !== undefined) {
86
- const type = checker.getTypeAtLocation(tsNode);
87
-
88
- // Check if it's an array type or tuple type
89
- const isArrayType =
90
- checker.isArrayType(type) || checker.isTupleType(type);
91
-
92
- if (!isArrayType) {
93
- return;
94
- }
95
- } else {
96
- return;
97
- }
98
- } else {
83
+ if (!isArrayOrTupleExpression(services, arrayExpression)) {
99
84
  return;
100
85
  }
101
86
 
102
87
  mut_nodesToFix.push({
103
88
  node,
104
89
  arrayExpression,
105
- lengthExpression: valueSide,
90
+ // The literal or constant itself: a wrapper such as `as number`
91
+ // would only widen the length the guard narrows to.
92
+ lengthExpression: skipTypeWrappers(valueSide),
106
93
  isNegated,
107
94
  });
108
95
  },
@@ -115,7 +102,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
115
102
  index,
116
103
  { node, arrayExpression, lengthExpression, isNegated },
117
104
  ] of mut_nodesToFix.entries()) {
118
- const arrayText = sourceCode.getText(arrayExpression);
105
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
119
106
 
120
107
  const lengthText = sourceCode.getText(lengthExpression);
121
108
 
@@ -148,10 +135,3 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
148
135
  },
149
136
  defaultOptions: [],
150
137
  } as const;
151
-
152
- const isLengthAccess = (
153
- node: DeepReadonly<TSESTree.Expression>,
154
- ): node is TSESTree.MemberExpression =>
155
- node.type === AST_NODE_TYPES.MemberExpression &&
156
- node.property.type === AST_NODE_TYPES.Identifier &&
157
- node.property.name === 'length';
@@ -212,3 +212,66 @@ describe('prefer-arr-is-fixed-length-array', () => {
212
212
  ],
213
213
  });
214
214
  }, 20000);
215
+
216
+ describe('prefer-arr-is-fixed-length-array through type wrappers', () => {
217
+ tester.run('prefer-arr-is-fixed-length-array', preferArrIsFixedLengthArray, {
218
+ valid: [
219
+ {
220
+ name: 'a computed index named `length` is not the length',
221
+ code: dedent`
222
+ declare const xs: readonly number[];
223
+ declare const length: number;
224
+ const ok = xs[length] === 3;
225
+ `,
226
+ },
227
+ ],
228
+ invalid: [
229
+ {
230
+ name: 'the length or the bound wrapped',
231
+ code: dedent`
232
+ import { Arr } from 'ts-data-forge';
233
+ declare const xs: readonly number[];
234
+ const a = xs.length === (3 satisfies number);
235
+ const b = (xs.length as number) !== 3;
236
+ const c = (2 as number) === xs.length;
237
+ `,
238
+ output: dedent`
239
+ import { Arr } from 'ts-data-forge';
240
+ declare const xs: readonly number[];
241
+ const a = Arr.isFixedLengthArray(3, xs);
242
+ const b = !Arr.isFixedLengthArray(3, xs);
243
+ const c = Arr.isFixedLengthArray(2, xs);
244
+ `,
245
+ errors: [
246
+ { messageId: 'useIsFixedLengthArray' },
247
+ { messageId: 'useIsFixedLengthArray' },
248
+ { messageId: 'useIsFixedLengthArray' },
249
+ ],
250
+ },
251
+ ],
252
+ });
253
+ }, 20000);
254
+
255
+ describe('prefer-arr-is-fixed-length-array with parenthesized operands', () => {
256
+ tester.run('prefer-arr-is-fixed-length-array', preferArrIsFixedLengthArray, {
257
+ valid: [],
258
+ invalid: [
259
+ {
260
+ name: 'a sequence array keeps its parentheses',
261
+ code: dedent`
262
+ import { Arr } from 'ts-data-forge';
263
+ declare const xs: readonly number[];
264
+ declare const log: () => void;
265
+ const ok = (log(), xs).length === 2;
266
+ `,
267
+ output: dedent`
268
+ import { Arr } from 'ts-data-forge';
269
+ declare const xs: readonly number[];
270
+ declare const log: () => void;
271
+ const ok = Arr.isFixedLengthArray(2, (log(), xs));
272
+ `,
273
+ errors: [{ messageId: 'useIsFixedLengthArray' }],
274
+ },
275
+ ],
276
+ });
277
+ }, 20000);
@@ -1,8 +1,11 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
2
  import {
3
+ asLengthAccess,
4
+ isArrayOrTupleExpression,
3
5
  isIntegerLiteralOrConstant,
4
- isLengthAccess,
5
6
  isPartOfBoundedLengthCheck,
7
+ skipTypeWrappers,
8
+ toArgumentText,
6
9
  } from './ast-utils.mjs';
7
10
  import {
8
11
  buildImportFixes,
@@ -63,12 +66,15 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
63
66
  // xs.length <= n or n >= xs.length
64
67
  const isLengthOnLeft = node.operator === '<=';
65
68
 
66
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
69
+ // Both sides are read through type wrappers.
70
+ const lengthSide = asLengthAccess(
71
+ node[isLengthOnLeft ? 'left' : 'right'],
72
+ );
67
73
 
68
74
  const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
69
75
 
70
76
  // Check if lengthSide is accessing .length
71
- if (!isLengthAccess(lengthSide)) {
77
+ if (lengthSide === undefined) {
72
78
  return;
73
79
  }
74
80
 
@@ -77,36 +83,19 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
77
83
  return;
78
84
  }
79
85
 
80
- // lengthSide is MemberExpression accessing .length
81
86
  const arrayExpression = lengthSide.object;
82
87
 
83
88
  // Check if arrayExpression is actually an array type
84
- if (services?.program !== undefined && services.program !== null) {
85
- const checker = services.program.getTypeChecker();
86
-
87
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
88
-
89
- if (tsNode !== undefined) {
90
- const type = checker.getTypeAtLocation(tsNode);
91
-
92
- // Check if it's an array type or tuple type
93
- const isArrayType =
94
- checker.isArrayType(type) || checker.isTupleType(type);
95
-
96
- if (!isArrayType) {
97
- return;
98
- }
99
- } else {
100
- return;
101
- }
102
- } else {
89
+ if (!isArrayOrTupleExpression(services, arrayExpression)) {
103
90
  return;
104
91
  }
105
92
 
106
93
  mut_nodesToFix.push({
107
94
  node,
108
95
  arrayExpression,
109
- lengthExpression: valueSide,
96
+ // The literal or constant itself: a wrapper such as `as number`
97
+ // would only widen the length the guard narrows to.
98
+ lengthExpression: skipTypeWrappers(valueSide),
110
99
  });
111
100
  },
112
101
  'Program:exit': () => {
@@ -118,7 +107,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
118
107
  index,
119
108
  { node, arrayExpression, lengthExpression },
120
109
  ] of mut_nodesToFix.entries()) {
121
- const arrayText = sourceCode.getText(arrayExpression);
110
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
122
111
 
123
112
  const lengthText = sourceCode.getText(lengthExpression);
124
113
 
@@ -186,3 +186,70 @@ describe('prefer-arr-is-max-length-array', () => {
186
186
  ],
187
187
  });
188
188
  }, 20000);
189
+
190
+ describe('prefer-arr-is-max-length-array through type wrappers', () => {
191
+ tester.run('prefer-arr-is-max-length-array', preferArrIsMaxLengthArray, {
192
+ valid: [
193
+ {
194
+ name: 'a computed index named `length` is not the length',
195
+ code: dedent`
196
+ declare const xs: readonly number[];
197
+ declare const length: number;
198
+ const ok = xs[length] <= 2;
199
+ `,
200
+ },
201
+ {
202
+ name: 'the upper half of a bounded pair whose other half is wrapped',
203
+ code: dedent`
204
+ declare const xs: readonly number[];
205
+ const ok = xs!.length >= 1 && xs.length <= 3;
206
+ `,
207
+ },
208
+ ],
209
+ invalid: [
210
+ {
211
+ name: 'the length or the bound wrapped',
212
+ code: dedent`
213
+ import { Arr } from 'ts-data-forge';
214
+ declare const xs: readonly number[];
215
+ const a = (xs.length satisfies number) <= 2;
216
+ const b = xs.length <= (2 as number);
217
+ `,
218
+ output: dedent`
219
+ import { Arr } from 'ts-data-forge';
220
+ declare const xs: readonly number[];
221
+ const a = Arr.isMaxLengthArray(2, xs);
222
+ const b = Arr.isMaxLengthArray(2, xs);
223
+ `,
224
+ errors: [
225
+ { messageId: 'useIsMaxLengthArray' },
226
+ { messageId: 'useIsMaxLengthArray' },
227
+ ],
228
+ },
229
+ ],
230
+ });
231
+ }, 20000);
232
+
233
+ describe('prefer-arr-is-max-length-array with parenthesized operands', () => {
234
+ tester.run('prefer-arr-is-max-length-array', preferArrIsMaxLengthArray, {
235
+ valid: [],
236
+ invalid: [
237
+ {
238
+ name: 'a sequence array keeps its parentheses',
239
+ code: dedent`
240
+ import { Arr } from 'ts-data-forge';
241
+ declare const xs: readonly number[];
242
+ declare const log: () => void;
243
+ const ok = (log(), xs).length <= 2;
244
+ `,
245
+ output: dedent`
246
+ import { Arr } from 'ts-data-forge';
247
+ declare const xs: readonly number[];
248
+ declare const log: () => void;
249
+ const ok = Arr.isMaxLengthArray(2, (log(), xs));
250
+ `,
251
+ errors: [{ messageId: 'useIsMaxLengthArray' }],
252
+ },
253
+ ],
254
+ });
255
+ }, 20000);