eslint-plugin-ts-data-forge 0.8.1 → 0.8.3

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 (139) hide show
  1. package/dist/plugin.d.mts +1 -1
  2. package/dist/plugin.d.mts.map +1 -1
  3. package/dist/plugin.mjs +1 -0
  4. package/dist/plugin.mjs.map +1 -1
  5. package/dist/rule-types.d.mts +3 -3
  6. package/dist/rule-types.d.mts.map +1 -1
  7. package/dist/rules/ast-utils.d.mts +59 -3
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +142 -31
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/branded-number-types.mjs.map +1 -1
  12. package/dist/rules/import-utils.d.mts +1 -1
  13. package/dist/rules/import-utils.d.mts.map +1 -1
  14. package/dist/rules/import-utils.mjs +26 -13
  15. package/dist/rules/import-utils.mjs.map +1 -1
  16. package/dist/rules/no-side-effect-import.d.mts +1 -1
  17. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  19. package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
  20. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  21. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-is-array.mjs +25 -11
  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 +11 -21
  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 +16 -28
  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 +16 -24
  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 +16 -24
  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 +9 -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 +68 -31
  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 +57 -40
  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 +59 -30
  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 +27 -24
  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 +59 -28
  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 +41 -16
  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 +18 -8
  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 +31 -6
  67. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  68. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  69. package/dist/rules/prefer-is-non-null-object.mjs +56 -40
  70. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  71. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  72. package/dist/rules/prefer-is-record-and-has-key.mjs +18 -8
  73. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  74. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  75. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
  77. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  78. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
  80. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  81. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  84. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.mjs +15 -8
  86. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  87. package/dist/types.d.mts +2 -2
  88. package/dist/types.d.mts.map +1 -1
  89. package/package.json +25 -23
  90. package/src/plugin.mts +5 -4
  91. package/src/plugin.test.mts +0 -13
  92. package/src/rule-types.mts +3 -3
  93. package/src/rules/ast-utils.mts +192 -37
  94. package/src/rules/branded-number-types.mts +1 -1
  95. package/src/rules/import-utils.mts +41 -17
  96. package/src/rules/no-side-effect-import.mts +1 -1
  97. package/src/rules/no-unnecessary-type-guard.mts +249 -148
  98. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  99. package/src/rules/prefer-arr-is-array.mts +32 -13
  100. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  101. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
  102. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  103. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  104. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  105. package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
  106. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  107. package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
  108. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  109. package/src/rules/prefer-arr-is-non-empty.mts +19 -24
  110. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  111. package/src/rules/prefer-arr-scan.mts +107 -37
  112. package/src/rules/prefer-arr-scan.test.mts +149 -0
  113. package/src/rules/prefer-arr-sum.mts +87 -50
  114. package/src/rules/prefer-arr-sum.test.mts +97 -0
  115. package/src/rules/prefer-arr-uniq.mts +83 -33
  116. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  117. package/src/rules/prefer-as-int.mts +38 -38
  118. package/src/rules/prefer-as-int.test.mts +51 -0
  119. package/src/rules/prefer-canonical-array-slicing.mts +84 -29
  120. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  121. package/src/rules/prefer-canonical-length-cast.mts +56 -18
  122. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  123. package/src/rules/prefer-canonical-length-guard.mts +25 -11
  124. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  125. package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
  126. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  127. package/src/rules/prefer-is-non-null-object.mts +57 -33
  128. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  129. package/src/rules/prefer-is-record-and-has-key.mts +27 -12
  130. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  131. package/src/rules/prefer-num-safe-parse-float.mts +37 -13
  132. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  133. package/src/rules/prefer-num-safe-parse-int.mts +41 -15
  134. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  135. package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
  136. package/src/rules/prefer-range-for-loop.mts +21 -9
  137. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  138. package/src/rules/rules.mts +1 -1
  139. package/src/types.mts +2 -2
@@ -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,
@@ -47,20 +52,24 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
47
52
  return {
48
53
  LogicalExpression: (node) => {
49
54
  // Only `a && b` can encode a `min && max` length range.
50
- if (node.operator !== '&&') return;
55
+ if (node.operator !== '&&') {
56
+ return;
57
+ }
51
58
 
52
59
  const left = parseLengthComparison(node.left, sourceCode);
53
60
 
54
61
  const right = parseLengthComparison(node.right, sourceCode);
55
62
 
56
- if (left === undefined || right === undefined) return;
63
+ if (left === undefined || right === undefined) {
64
+ return;
65
+ }
57
66
 
58
67
  // Need one lower bound and one upper bound on the same array.
59
- if (left.kind === right.kind) return;
68
+ if (left.kind === right.kind) {
69
+ return;
70
+ }
60
71
 
61
- if (
62
- sourceCode.getText(left.array) !== sourceCode.getText(right.array)
63
- ) {
72
+ if (!isSameArrayText(left.array, right.array, sourceCode)) {
64
73
  return;
65
74
  }
66
75
 
@@ -71,23 +80,10 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
71
80
  const arrayExpression = min.array;
72
81
 
73
82
  // Check if arrayExpression is actually an array type
74
- if (services?.program !== undefined && services.program !== null) {
75
- const checker = services.program.getTypeChecker();
76
-
77
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
78
-
79
- if (tsNode !== undefined) {
80
- const type = checker.getTypeAtLocation(tsNode);
81
-
82
- // Check if it's an array type or tuple type
83
- const isArrayType =
84
- checker.isArrayType(type) || checker.isTupleType(type);
85
-
86
- if (!isArrayType) return;
87
- } else {
88
- return;
89
- }
90
- } else {
83
+ if (
84
+ !isArrayOrTupleExpression(services, min.array) ||
85
+ !isArrayOrTupleExpression(services, max.array)
86
+ ) {
91
87
  return;
92
88
  }
93
89
 
@@ -107,7 +103,7 @@ export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
107
103
  index,
108
104
  { node, arrayExpression, minExpression, maxExpression },
109
105
  ] of mut_nodesToFix.entries()) {
110
- const arrayText = sourceCode.getText(arrayExpression);
106
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
111
107
 
112
108
  const minText = sourceCode.getText(minExpression);
113
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,
@@ -52,49 +53,43 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
52
53
  return {
53
54
  BinaryExpression: (node) => {
54
55
  // Check for `xs.length === n` or `n === xs.length` or `xs.length !== n`
55
- if (node.operator !== '===' && node.operator !== '!==') return;
56
+ if (node.operator !== '===' && node.operator !== '!==') {
57
+ return;
58
+ }
56
59
 
57
60
  const isNegated = node.operator === '!==';
58
61
 
59
- const isLengthOnLeft = isLengthAccess(node.left);
62
+ // Both sides are read through type wrappers.
63
+ const leftLength = asLengthAccess(node.left);
64
+
65
+ const isLengthOnLeft = leftLength !== undefined;
60
66
 
61
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
67
+ const lengthSide = leftLength ?? asLengthAccess(node.right);
62
68
 
63
69
  const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
64
70
 
65
- if (!isLengthAccess(lengthSide)) return;
71
+ if (lengthSide === undefined) {
72
+ return;
73
+ }
66
74
 
67
75
  // Only match integer literals or const variables initialized with integer literals
68
- if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) return;
76
+ if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
77
+ return;
78
+ }
69
79
 
70
- // lengthSide is MemberExpression accessing .length
71
80
  const arrayExpression = lengthSide.object;
72
81
 
73
82
  // Check if arrayExpression is actually an array type
74
- if (services?.program !== undefined && services.program !== null) {
75
- const checker = services.program.getTypeChecker();
76
-
77
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
78
-
79
- if (tsNode !== undefined) {
80
- const type = checker.getTypeAtLocation(tsNode);
81
-
82
- // Check if it's an array type or tuple type
83
- const isArrayType =
84
- checker.isArrayType(type) || checker.isTupleType(type);
85
-
86
- if (!isArrayType) return;
87
- } else {
88
- return;
89
- }
90
- } else {
83
+ if (!isArrayOrTupleExpression(services, arrayExpression)) {
91
84
  return;
92
85
  }
93
86
 
94
87
  mut_nodesToFix.push({
95
88
  node,
96
89
  arrayExpression,
97
- 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),
98
93
  isNegated,
99
94
  });
100
95
  },
@@ -107,7 +102,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
107
102
  index,
108
103
  { node, arrayExpression, lengthExpression, isNegated },
109
104
  ] of mut_nodesToFix.entries()) {
110
- const arrayText = sourceCode.getText(arrayExpression);
105
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
111
106
 
112
107
  const lengthText = sourceCode.getText(lengthExpression);
113
108
 
@@ -140,10 +135,3 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
140
135
  },
141
136
  defaultOptions: [],
142
137
  } as const;
143
-
144
- const isLengthAccess = (
145
- node: DeepReadonly<TSESTree.Expression>,
146
- ): node is TSESTree.MemberExpression =>
147
- node.type === AST_NODE_TYPES.MemberExpression &&
148
- node.property.type === AST_NODE_TYPES.Identifier &&
149
- 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,
@@ -50,53 +53,49 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
50
53
  return {
51
54
  BinaryExpression: (node) => {
52
55
  // Check for `xs.length <= n` or `n >= xs.length`
53
- if (node.operator !== '>=' && node.operator !== '<=') return;
56
+ if (node.operator !== '>=' && node.operator !== '<=') {
57
+ return;
58
+ }
54
59
 
55
60
  // Defer to `prefer-arr-is-bounded-length-array` when this comparison is
56
61
  // the upper bound of a `min && max` pair on the same array.
57
- if (isPartOfBoundedLengthCheck(node, sourceCode)) return;
62
+ if (isPartOfBoundedLengthCheck(node, sourceCode)) {
63
+ return;
64
+ }
58
65
 
59
66
  // xs.length <= n or n >= xs.length
60
67
  const isLengthOnLeft = node.operator === '<=';
61
68
 
62
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
69
+ // Both sides are read through type wrappers.
70
+ const lengthSide = asLengthAccess(
71
+ node[isLengthOnLeft ? 'left' : 'right'],
72
+ );
63
73
 
64
74
  const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
65
75
 
66
76
  // Check if lengthSide is accessing .length
67
- if (!isLengthAccess(lengthSide)) return;
77
+ if (lengthSide === undefined) {
78
+ return;
79
+ }
68
80
 
69
81
  // Only match integer literals or const variables initialized with integer literals
70
- if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) return;
82
+ if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
83
+ return;
84
+ }
71
85
 
72
- // lengthSide is MemberExpression accessing .length
73
86
  const arrayExpression = lengthSide.object;
74
87
 
75
88
  // Check if arrayExpression is actually an array type
76
- if (services?.program !== undefined && services.program !== null) {
77
- const checker = services.program.getTypeChecker();
78
-
79
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
80
-
81
- if (tsNode !== undefined) {
82
- const type = checker.getTypeAtLocation(tsNode);
83
-
84
- // Check if it's an array type or tuple type
85
- const isArrayType =
86
- checker.isArrayType(type) || checker.isTupleType(type);
87
-
88
- if (!isArrayType) return;
89
- } else {
90
- return;
91
- }
92
- } else {
89
+ if (!isArrayOrTupleExpression(services, arrayExpression)) {
93
90
  return;
94
91
  }
95
92
 
96
93
  mut_nodesToFix.push({
97
94
  node,
98
95
  arrayExpression,
99
- 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),
100
99
  });
101
100
  },
102
101
  'Program:exit': () => {
@@ -108,7 +107,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
108
107
  index,
109
108
  { node, arrayExpression, lengthExpression },
110
109
  ] of mut_nodesToFix.entries()) {
111
- const arrayText = sourceCode.getText(arrayExpression);
110
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
112
111
 
113
112
  const lengthText = sourceCode.getText(lengthExpression);
114
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);
@@ -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,
@@ -50,53 +53,49 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
50
53
  return {
51
54
  BinaryExpression: (node) => {
52
55
  // Check for `xs.length >= n` or `n <= xs.length`
53
- if (node.operator !== '>=' && node.operator !== '<=') return;
56
+ if (node.operator !== '>=' && node.operator !== '<=') {
57
+ return;
58
+ }
54
59
 
55
60
  // Defer to `prefer-arr-is-bounded-length-array` when this comparison is
56
61
  // the lower bound of a `min && max` pair on the same array.
57
- if (isPartOfBoundedLengthCheck(node, sourceCode)) return;
62
+ if (isPartOfBoundedLengthCheck(node, sourceCode)) {
63
+ return;
64
+ }
58
65
 
59
66
  // xs.length >= n or n <= xs.length
60
67
  const isLengthOnLeft = node.operator === '>=';
61
68
 
62
- const lengthSide = node[isLengthOnLeft ? 'left' : 'right'];
69
+ // Both sides are read through type wrappers.
70
+ const lengthSide = asLengthAccess(
71
+ node[isLengthOnLeft ? 'left' : 'right'],
72
+ );
63
73
 
64
74
  const valueSide = node[isLengthOnLeft ? 'right' : 'left'];
65
75
 
66
76
  // Check if lengthSide is accessing .length
67
- if (!isLengthAccess(lengthSide)) return;
77
+ if (lengthSide === undefined) {
78
+ return;
79
+ }
68
80
 
69
81
  // Only match integer literals or const variables initialized with integer literals
70
- if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) return;
82
+ if (!isIntegerLiteralOrConstant(valueSide, sourceCode)) {
83
+ return;
84
+ }
71
85
 
72
- // lengthSide is MemberExpression accessing .length
73
86
  const arrayExpression = lengthSide.object;
74
87
 
75
88
  // Check if arrayExpression is actually an array type
76
- if (services?.program !== undefined && services.program !== null) {
77
- const checker = services.program.getTypeChecker();
78
-
79
- const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
80
-
81
- if (tsNode !== undefined) {
82
- const type = checker.getTypeAtLocation(tsNode);
83
-
84
- // Check if it's an array type or tuple type
85
- const isArrayType =
86
- checker.isArrayType(type) || checker.isTupleType(type);
87
-
88
- if (!isArrayType) return;
89
- } else {
90
- return;
91
- }
92
- } else {
89
+ if (!isArrayOrTupleExpression(services, arrayExpression)) {
93
90
  return;
94
91
  }
95
92
 
96
93
  mut_nodesToFix.push({
97
94
  node,
98
95
  arrayExpression,
99
- 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),
100
99
  });
101
100
  },
102
101
  'Program:exit': () => {
@@ -108,7 +107,7 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
108
107
  index,
109
108
  { node, arrayExpression, lengthExpression },
110
109
  ] of mut_nodesToFix.entries()) {
111
- const arrayText = sourceCode.getText(arrayExpression);
110
+ const arrayText = toArgumentText(arrayExpression, sourceCode);
112
111
 
113
112
  const lengthText = sourceCode.getText(lengthExpression);
114
113