eslint-plugin-ts-data-forge 0.2.0 → 0.4.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 (63) hide show
  1. package/README.md +16 -14
  2. package/dist/plugin.d.mts +3 -1
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +16 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rules/import-utils.mjs.map +1 -1
  9. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  10. package/dist/rules/no-unnecessary-type-guard.mjs +13 -1
  11. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  12. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +1 -1
  13. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  14. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
  15. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  16. package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
  17. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  18. package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
  19. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-sum.mjs +2 -2
  21. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  22. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  23. package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
  24. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
  25. package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
  26. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
  27. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  28. package/dist/rules/prefer-canonical-length-guard.mjs +12 -8
  29. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  30. package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -1
  31. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-record-and-has-key.mjs +30 -16
  33. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  34. package/dist/rules/prefer-range-for-loop.mjs +1 -1
  35. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  36. package/dist/rules/rules.d.mts +2 -1
  37. package/dist/rules/rules.d.mts.map +1 -1
  38. package/dist/rules/rules.mjs +2 -0
  39. package/dist/rules/rules.mjs.map +1 -1
  40. package/package.json +6 -6
  41. package/src/plugin.mts +1 -0
  42. package/src/rule-types.mts +17 -1
  43. package/src/rules/import-utils.mts +0 -1
  44. package/src/rules/no-unnecessary-type-guard.mts +65 -0
  45. package/src/rules/no-unnecessary-type-guard.test.mts +94 -0
  46. package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
  47. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
  48. package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
  49. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
  50. package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
  51. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
  52. package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
  53. package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
  54. package/src/rules/prefer-arr-sum.mts +2 -2
  55. package/src/rules/prefer-canonical-array-slicing.mts +0 -2
  56. package/src/rules/prefer-canonical-length-cast.mts +290 -0
  57. package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
  58. package/src/rules/prefer-canonical-length-guard.mts +40 -21
  59. package/src/rules/prefer-canonical-length-guard.test.mts +32 -32
  60. package/src/rules/prefer-is-record-and-has-key.mts +84 -20
  61. package/src/rules/prefer-is-record-and-has-key.test.mts +74 -0
  62. package/src/rules/prefer-range-for-loop.mts +1 -1
  63. package/src/rules/rules.mts +2 -0
@@ -85,7 +85,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
85
85
  output: dedent`
86
86
  import { Arr } from 'ts-data-forge';
87
87
  const xs: readonly number[] = [1, 2, 3];
88
- const ok = Arr.isBoundedLengthArray(xs, 1, 5);
88
+ const ok = Arr.isBoundedLengthArray(1, 5, xs);
89
89
  `,
90
90
  errors: [{ messageId: 'useIsBoundedLengthArray' }],
91
91
  },
@@ -98,7 +98,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
98
98
  output: dedent`
99
99
  import { Arr } from 'ts-data-forge';
100
100
  const xs: readonly number[] = [1, 2, 3];
101
- const ok = Arr.isBoundedLengthArray(xs, 1, 5);
101
+ const ok = Arr.isBoundedLengthArray(1, 5, xs);
102
102
  `,
103
103
  errors: [{ messageId: 'useIsBoundedLengthArray' }],
104
104
  },
@@ -111,7 +111,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
111
111
  output: dedent`
112
112
  import { Arr } from 'ts-data-forge';
113
113
  const xs: readonly number[] = [1, 2, 3];
114
- const ok = Arr.isBoundedLengthArray(xs, 1, 5);
114
+ const ok = Arr.isBoundedLengthArray(1, 5, xs);
115
115
  `,
116
116
  errors: [{ messageId: 'useIsBoundedLengthArray' }],
117
117
  },
@@ -128,7 +128,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
128
128
  const xs = [1, 2, 3];
129
129
  const lo = 1;
130
130
  const hi = 5;
131
- const ok = Arr.isBoundedLengthArray(xs, lo, hi);
131
+ const ok = Arr.isBoundedLengthArray(lo, hi, xs);
132
132
  `,
133
133
  errors: [{ messageId: 'useIsBoundedLengthArray' }],
134
134
  },
@@ -144,7 +144,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
144
144
  import { Arr } from 'ts-data-forge';
145
145
 
146
146
  const xs = [1, 2, 3];
147
- const ok = Arr.isBoundedLengthArray(xs, 1, 5);
147
+ const ok = Arr.isBoundedLengthArray(1, 5, xs);
148
148
  `,
149
149
  errors: [{ messageId: 'useIsBoundedLengthArray' }],
150
150
  },
@@ -122,7 +122,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
122
122
  length: lengthText,
123
123
  },
124
124
  fix: (fixer) => {
125
- const baseCall = `Arr.isFixedLengthArray(${arrayText}, ${lengthText})`;
125
+ const baseCall = `Arr.isFixedLengthArray(${lengthText}, ${arrayText})`;
126
126
 
127
127
  const replacement = isNegated ? `!${baseCall}` : baseCall;
128
128
 
@@ -76,7 +76,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
76
76
  output: dedent`
77
77
  import { Arr } from 'ts-data-forge';
78
78
  const xs: readonly number[] = [1, 2, 3];
79
- const ok = Arr.isFixedLengthArray(xs, 3);
79
+ const ok = Arr.isFixedLengthArray(3, xs);
80
80
  `,
81
81
  errors: [{ messageId: 'useIsFixedLengthArray' }],
82
82
  },
@@ -89,7 +89,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
89
89
  output: dedent`
90
90
  import { Arr } from 'ts-data-forge';
91
91
  const xs: readonly number[] = [1, 2, 3];
92
- const ok = Arr.isFixedLengthArray(xs, 3);
92
+ const ok = Arr.isFixedLengthArray(3, xs);
93
93
  `,
94
94
  errors: [{ messageId: 'useIsFixedLengthArray' }],
95
95
  },
@@ -102,7 +102,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
102
102
  output: dedent`
103
103
  import { Arr } from 'ts-data-forge';
104
104
  const xs = [1, 2, 3];
105
- const ok = Arr.isFixedLengthArray(xs, 3);
105
+ const ok = Arr.isFixedLengthArray(3, xs);
106
106
  `,
107
107
  errors: [{ messageId: 'useIsFixedLengthArray' }],
108
108
  },
@@ -115,7 +115,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
115
115
  output: dedent`
116
116
  import { Arr } from 'ts-data-forge';
117
117
  const xs = [1, 2, 3] as const;
118
- const ok = Arr.isFixedLengthArray(xs, 3);
118
+ const ok = Arr.isFixedLengthArray(3, xs);
119
119
  `,
120
120
  errors: [{ messageId: 'useIsFixedLengthArray' }],
121
121
  },
@@ -130,7 +130,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
130
130
  import { Arr } from 'ts-data-forge';
131
131
  const xs = [1, 2, 3];
132
132
  const n = 3;
133
- const ok = Arr.isFixedLengthArray(xs, n);
133
+ const ok = Arr.isFixedLengthArray(n, xs);
134
134
  `,
135
135
  errors: [{ messageId: 'useIsFixedLengthArray' }],
136
136
  },
@@ -146,7 +146,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
146
146
  import { Arr } from 'ts-data-forge';
147
147
 
148
148
  const xs = [1, 2, 3];
149
- const ok = Arr.isFixedLengthArray(xs, 3);
149
+ const ok = Arr.isFixedLengthArray(3, xs);
150
150
  `,
151
151
  errors: [{ messageId: 'useIsFixedLengthArray' }],
152
152
  },
@@ -162,8 +162,8 @@ describe('prefer-arr-is-fixed-length-array', () => {
162
162
  import { Arr } from 'ts-data-forge';
163
163
  const xs = [1, 2, 3];
164
164
  const ys = [4, 5];
165
- const ok1 = Arr.isFixedLengthArray(xs, 3);
166
- const ok2 = Arr.isFixedLengthArray(ys, 2);
165
+ const ok1 = Arr.isFixedLengthArray(3, xs);
166
+ const ok2 = Arr.isFixedLengthArray(2, ys);
167
167
  `,
168
168
  errors: [
169
169
  { messageId: 'useIsFixedLengthArray' },
@@ -179,7 +179,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
179
179
  output: dedent`
180
180
  import { Arr } from 'ts-data-forge';
181
181
  const xs: readonly number[] = [1, 2, 3];
182
- const ok = !Arr.isFixedLengthArray(xs, 3);
182
+ const ok = !Arr.isFixedLengthArray(3, xs);
183
183
  `,
184
184
  errors: [{ messageId: 'useIsFixedLengthArray' }],
185
185
  },
@@ -192,7 +192,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
192
192
  output: dedent`
193
193
  import { Arr } from 'ts-data-forge';
194
194
  const xs: readonly number[] = [1, 2, 3];
195
- const ok = !Arr.isFixedLengthArray(xs, 3);
195
+ const ok = !Arr.isFixedLengthArray(3, xs);
196
196
  `,
197
197
  errors: [{ messageId: 'useIsFixedLengthArray' }],
198
198
  },
@@ -205,7 +205,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
205
205
  output: dedent`
206
206
  import { Arr } from 'ts-data-forge';
207
207
  const xs = [1, 2, 3];
208
- const ok = !Arr.isFixedLengthArray(xs, 5);
208
+ const ok = !Arr.isFixedLengthArray(5, xs);
209
209
  `,
210
210
  errors: [{ messageId: 'useIsFixedLengthArray' }],
211
211
  },
@@ -123,7 +123,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
123
123
  length: lengthText,
124
124
  },
125
125
  fix: (fixer) => {
126
- const replacement = `Arr.isMaxLengthArray(${arrayText}, ${lengthText})`;
126
+ const replacement = `Arr.isMaxLengthArray(${lengthText}, ${arrayText})`;
127
127
 
128
128
  const importFixes =
129
129
  index === 0 && !hasArrImport
@@ -89,7 +89,7 @@ describe('prefer-arr-is-max-length-array', () => {
89
89
  output: dedent`
90
90
  import { Arr } from 'ts-data-forge';
91
91
  const xs: readonly number[] = [1, 2, 3];
92
- const ok = Arr.isMaxLengthArray(xs, 3);
92
+ const ok = Arr.isMaxLengthArray(3, xs);
93
93
  `,
94
94
  errors: [{ messageId: 'useIsMaxLengthArray' }],
95
95
  },
@@ -102,7 +102,7 @@ describe('prefer-arr-is-max-length-array', () => {
102
102
  output: dedent`
103
103
  import { Arr } from 'ts-data-forge';
104
104
  const xs: readonly number[] = [1, 2, 3];
105
- const ok = Arr.isMaxLengthArray(xs, 3);
105
+ const ok = Arr.isMaxLengthArray(3, xs);
106
106
  `,
107
107
  errors: [{ messageId: 'useIsMaxLengthArray' }],
108
108
  },
@@ -115,7 +115,7 @@ describe('prefer-arr-is-max-length-array', () => {
115
115
  output: dedent`
116
116
  import { Arr } from 'ts-data-forge';
117
117
  const xs = [1, 2, 3];
118
- const ok = Arr.isMaxLengthArray(xs, 1);
118
+ const ok = Arr.isMaxLengthArray(1, xs);
119
119
  `,
120
120
  errors: [{ messageId: 'useIsMaxLengthArray' }],
121
121
  },
@@ -128,7 +128,7 @@ describe('prefer-arr-is-max-length-array', () => {
128
128
  output: dedent`
129
129
  import { Arr } from 'ts-data-forge';
130
130
  const xs = [1, 2, 3] as const;
131
- const ok = Arr.isMaxLengthArray(xs, 2);
131
+ const ok = Arr.isMaxLengthArray(2, xs);
132
132
  `,
133
133
  errors: [{ messageId: 'useIsMaxLengthArray' }],
134
134
  },
@@ -143,7 +143,7 @@ describe('prefer-arr-is-max-length-array', () => {
143
143
  import { Arr } from 'ts-data-forge';
144
144
  const xs = [1, 2, 3];
145
145
  const n = 2;
146
- const ok = Arr.isMaxLengthArray(xs, n);
146
+ const ok = Arr.isMaxLengthArray(n, xs);
147
147
  `,
148
148
  errors: [{ messageId: 'useIsMaxLengthArray' }],
149
149
  },
@@ -159,7 +159,7 @@ describe('prefer-arr-is-max-length-array', () => {
159
159
  import { Arr } from 'ts-data-forge';
160
160
 
161
161
  const xs = [1, 2, 3];
162
- const ok = Arr.isMaxLengthArray(xs, 1);
162
+ const ok = Arr.isMaxLengthArray(1, xs);
163
163
  `,
164
164
  errors: [{ messageId: 'useIsMaxLengthArray' }],
165
165
  },
@@ -175,8 +175,8 @@ describe('prefer-arr-is-max-length-array', () => {
175
175
  import { Arr } from 'ts-data-forge';
176
176
  const xs = [1, 2, 3];
177
177
  const ys = [4, 5];
178
- const ok1 = Arr.isMaxLengthArray(xs, 2);
179
- const ok2 = Arr.isMaxLengthArray(ys, 1);
178
+ const ok1 = Arr.isMaxLengthArray(2, xs);
179
+ const ok2 = Arr.isMaxLengthArray(1, ys);
180
180
  `,
181
181
  errors: [
182
182
  { messageId: 'useIsMaxLengthArray' },
@@ -123,7 +123,7 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
123
123
  length: lengthText,
124
124
  },
125
125
  fix: (fixer) => {
126
- const replacement = `Arr.isMinLengthArray(${arrayText}, ${lengthText})`;
126
+ const replacement = `Arr.isMinLengthArray(${lengthText}, ${arrayText})`;
127
127
 
128
128
  const importFixes =
129
129
  index === 0 && !hasArrImport
@@ -90,7 +90,7 @@ describe('prefer-arr-is-min-length-array', () => {
90
90
  output: dedent`
91
91
  import { Arr } from 'ts-data-forge';
92
92
  const xs: readonly number[] = [1, 2, 3];
93
- const ok = Arr.isMinLengthArray(xs, 3);
93
+ const ok = Arr.isMinLengthArray(3, xs);
94
94
  `,
95
95
  errors: [{ messageId: 'useIsMinLengthArray' }],
96
96
  },
@@ -103,7 +103,7 @@ describe('prefer-arr-is-min-length-array', () => {
103
103
  output: dedent`
104
104
  import { Arr } from 'ts-data-forge';
105
105
  const xs: readonly number[] = [1, 2, 3];
106
- const ok = Arr.isMinLengthArray(xs, 3);
106
+ const ok = Arr.isMinLengthArray(3, xs);
107
107
  `,
108
108
  errors: [{ messageId: 'useIsMinLengthArray' }],
109
109
  },
@@ -116,7 +116,7 @@ describe('prefer-arr-is-min-length-array', () => {
116
116
  output: dedent`
117
117
  import { Arr } from 'ts-data-forge';
118
118
  const xs = [1, 2, 3];
119
- const ok = Arr.isMinLengthArray(xs, 1);
119
+ const ok = Arr.isMinLengthArray(1, xs);
120
120
  `,
121
121
  errors: [{ messageId: 'useIsMinLengthArray' }],
122
122
  },
@@ -129,7 +129,7 @@ describe('prefer-arr-is-min-length-array', () => {
129
129
  output: dedent`
130
130
  import { Arr } from 'ts-data-forge';
131
131
  const xs = [1, 2, 3] as const;
132
- const ok = Arr.isMinLengthArray(xs, 2);
132
+ const ok = Arr.isMinLengthArray(2, xs);
133
133
  `,
134
134
  errors: [{ messageId: 'useIsMinLengthArray' }],
135
135
  },
@@ -144,7 +144,7 @@ describe('prefer-arr-is-min-length-array', () => {
144
144
  import { Arr } from 'ts-data-forge';
145
145
  const xs = [1, 2, 3];
146
146
  const n = 2;
147
- const ok = Arr.isMinLengthArray(xs, n);
147
+ const ok = Arr.isMinLengthArray(n, xs);
148
148
  `,
149
149
  errors: [{ messageId: 'useIsMinLengthArray' }],
150
150
  },
@@ -160,7 +160,7 @@ describe('prefer-arr-is-min-length-array', () => {
160
160
  import { Arr } from 'ts-data-forge';
161
161
 
162
162
  const xs = [1, 2, 3];
163
- const ok = Arr.isMinLengthArray(xs, 1);
163
+ const ok = Arr.isMinLengthArray(1, xs);
164
164
  `,
165
165
  errors: [{ messageId: 'useIsMinLengthArray' }],
166
166
  },
@@ -176,8 +176,8 @@ describe('prefer-arr-is-min-length-array', () => {
176
176
  import { Arr } from 'ts-data-forge';
177
177
  const xs = [1, 2, 3];
178
178
  const ys = [4, 5];
179
- const ok1 = Arr.isMinLengthArray(xs, 2);
180
- const ok2 = Arr.isMinLengthArray(ys, 1);
179
+ const ok1 = Arr.isMinLengthArray(2, xs);
180
+ const ok2 = Arr.isMinLengthArray(1, ys);
181
181
  `,
182
182
  errors: [
183
183
  { messageId: 'useIsMinLengthArray' },
@@ -193,7 +193,7 @@ describe('prefer-arr-is-min-length-array', () => {
193
193
  output: dedent`
194
194
  import { Arr } from 'ts-data-forge';
195
195
  const xs = [1, 2, 3];
196
- const ok = Arr.isMinLengthArray(xs, 1);
196
+ const ok = Arr.isMinLengthArray(1, xs);
197
197
  `,
198
198
  errors: [{ messageId: 'useIsMinLengthArray' }],
199
199
  },
@@ -61,7 +61,7 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
61
61
  const arrayExpression = node.callee.object;
62
62
 
63
63
  // Check if we have 2 arguments: reducer function and initial value 0
64
- if (!Arr.isFixedLengthTuple(node.arguments, 2)) return;
64
+ if (!Arr.isFixedLengthTuple(2, node.arguments)) return;
65
65
 
66
66
  const reducer = node.arguments[0];
67
67
 
@@ -78,7 +78,7 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
78
78
  // Check if reducer is an arrow function with 2 parameters
79
79
  if (
80
80
  reducer.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
81
- !Arr.isFixedLengthTuple(reducer.params, 2)
81
+ !Arr.isFixedLengthTuple(2, reducer.params)
82
82
  ) {
83
83
  return;
84
84
  }
@@ -796,5 +796,3 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
796
796
  },
797
797
  defaultOptions: [],
798
798
  } as const;
799
-
800
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -0,0 +1,290 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
7
+
8
+ type Options = readonly [];
9
+
10
+ type MessageIds = 'useCanonicalCast';
11
+
12
+ /** How one leading length argument of a cast call must look. */
13
+ type BoundMatcher = number | 'anyLength' | 'sameAsFirst';
14
+
15
+ type CastRewrite = Readonly<{
16
+ /** The `Arr` cast the call resolves to. */
17
+ cast: string;
18
+ /** One matcher per leading length argument the call must have. */
19
+ bounds: readonly BoundMatcher[];
20
+ /** The cast the call is rewritten to. */
21
+ replacement: string;
22
+ /** Indices into `bounds` of the length arguments the rewrite keeps, in order. */
23
+ keep: readonly number[];
24
+ }>;
25
+
26
+ /**
27
+ * `Arr.as*` casts whose length arguments make them a longhand spelling of
28
+ * another cast.
29
+ *
30
+ * Unlike a `Type<T>` wrapper, a cast only ever *returns* the narrowed value, so
31
+ * a rewrite that strengthens the result type stays safe — the result is
32
+ * assignable everywhere the old one was:
33
+ *
34
+ * | call | canonical form | relation |
35
+ * | :----------------------------------- | :------------------------------ | :------- |
36
+ * | `Arr.asMinLengthArray(1, xs)` | `Arr.asNonEmptyArray(xs)` | type-identical — `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` |
37
+ * | `Arr.asFixedLengthArray(0, xs)` | `Arr.asEmptyArray(xs)` | type-identical — `asEmptyArray` is the length-0 specialization |
38
+ * | `Arr.asBoundedLengthArray(n, n, xs)` | `Arr.asFixedLengthArray(n, xs)` | strengthens — `FixedLengthArray<N, E>` is `BoundedLengthArray<N, N, E>` intersected with the exact tuple for `N <= 10` |
39
+ * | `Arr.asMaxLengthArray(0, xs)` | `Arr.asEmptyArray(xs)` | strengthens — an array of at most `0` elements is an array of exactly `0` |
40
+ * | `Arr.asMinLengthTuple(1, xs)` | `Arr.asNonEmptyTuple(xs)` | type-identical — both are `MinLengthTuple<1, E>` |
41
+ * | `Arr.asFixedLengthTuple(0, xs)` | `Arr.asEmptyTuple(xs)` | type-identical — both are `readonly []` |
42
+ * | `Arr.asBoundedLengthTuple(n, n, xs)` | `Arr.asFixedLengthTuple(n, xs)` | type-identical — `BoundedLengthTuple<N, N, E>` *is* `FixedLengthTuple<N, E>` |
43
+ * | `Arr.asMaxLengthTuple(0, xs)` | `Arr.asEmptyTuple(xs)` | type-identical — `MaxLengthTuple<0, E>` is `readonly []` |
44
+ *
45
+ * Each family is rewritten within its own lane — the branded `*Array` casts to
46
+ * `asEmptyArray` / `asNonEmptyArray`, the structural `*Tuple` casts to
47
+ * `asEmptyTuple` / `asNonEmptyTuple` — so a rewrite never moves a value across
48
+ * the branded/structural divide, matching `prefer-canonical-length-guard`.
49
+ *
50
+ * `Arr.asBoundedLengthArray(0, n, xs)` → `Arr.asMaxLengthArray(n, xs)` is
51
+ * deliberately absent: it would *drop* the `MinLengthArray<0, E>` brand, which
52
+ * is a widening rather than a rename.
53
+ *
54
+ * Only the runtime `TypeError` message changes, to the one that names the
55
+ * constraint actually being checked.
56
+ */
57
+ const CAST_REWRITES = [
58
+ {
59
+ cast: 'asMinLengthArray',
60
+ bounds: [1],
61
+ replacement: 'asNonEmptyArray',
62
+ keep: [],
63
+ },
64
+ {
65
+ cast: 'asFixedLengthArray',
66
+ bounds: [0],
67
+ replacement: 'asEmptyArray',
68
+ keep: [],
69
+ },
70
+ // Listed before the `anyLength, sameAsFirst` entry so that
71
+ // `asBoundedLengthArray(0, 0, xs)` lands on `asEmptyArray` in one step.
72
+ {
73
+ cast: 'asBoundedLengthArray',
74
+ bounds: [0, 0],
75
+ replacement: 'asEmptyArray',
76
+ keep: [],
77
+ },
78
+ {
79
+ cast: 'asBoundedLengthArray',
80
+ bounds: ['anyLength', 'sameAsFirst'],
81
+ replacement: 'asFixedLengthArray',
82
+ keep: [0],
83
+ },
84
+ {
85
+ cast: 'asMaxLengthArray',
86
+ bounds: [0],
87
+ replacement: 'asEmptyArray',
88
+ keep: [],
89
+ },
90
+ {
91
+ cast: 'asMinLengthTuple',
92
+ bounds: [1],
93
+ replacement: 'asNonEmptyTuple',
94
+ keep: [],
95
+ },
96
+ {
97
+ cast: 'asFixedLengthTuple',
98
+ bounds: [0],
99
+ replacement: 'asEmptyTuple',
100
+ keep: [],
101
+ },
102
+ {
103
+ cast: 'asBoundedLengthTuple',
104
+ bounds: [0, 0],
105
+ replacement: 'asEmptyTuple',
106
+ keep: [],
107
+ },
108
+ {
109
+ cast: 'asBoundedLengthTuple',
110
+ bounds: ['anyLength', 'sameAsFirst'],
111
+ replacement: 'asFixedLengthTuple',
112
+ keep: [0],
113
+ },
114
+ {
115
+ cast: 'asMaxLengthTuple',
116
+ bounds: [0],
117
+ replacement: 'asEmptyTuple',
118
+ keep: [],
119
+ },
120
+ ] as const satisfies readonly CastRewrite[];
121
+
122
+ export const preferCanonicalLengthCast: TSESLint.RuleModule<
123
+ MessageIds,
124
+ Options
125
+ > = {
126
+ meta: {
127
+ type: 'suggestion',
128
+ docs: {
129
+ description:
130
+ 'Normalize an `Arr` length cast whose bounds are degenerate (e.g. `Arr.asMinLengthArray(1, xs)`, `Arr.asFixedLengthTuple(0, xs)`) to the cast that names that constraint directly, staying within the branded `*Array` or structural `*Tuple` family.',
131
+ },
132
+ fixable: 'code',
133
+ schema: [],
134
+ messages: {
135
+ useCanonicalCast:
136
+ 'Use `{{arrName}}.{{replacement}}(...)` instead of `{{arrName}}.{{cast}}({{boundsText}})`: with these bounds the result type is the same (or stricter), and `{{replacement}}` names the constraint directly.',
137
+ },
138
+ },
139
+
140
+ create: (context) => {
141
+ const sourceCode = context.sourceCode;
142
+
143
+ const arrLocalName = getImportedLocalName(
144
+ getTsDataForgeImport(sourceCode.ast),
145
+ 'Arr',
146
+ );
147
+
148
+ if (arrLocalName === undefined) return {};
149
+
150
+ return {
151
+ CallExpression: (node) => {
152
+ const castName = getCastName(node, arrLocalName);
153
+
154
+ if (castName === undefined) return;
155
+
156
+ const rewrite = CAST_REWRITES.find(
157
+ (candidate) =>
158
+ candidate.cast === castName && matchesBounds(node, candidate),
159
+ );
160
+
161
+ if (rewrite === undefined) return;
162
+
163
+ const boundsText = node.arguments
164
+ .slice(0, rewrite.bounds.length)
165
+ .map((arg) => sourceCode.getText(arg))
166
+ .join(', ');
167
+
168
+ context.report({
169
+ node,
170
+ messageId: 'useCanonicalCast',
171
+ data: {
172
+ arrName: arrLocalName,
173
+ cast: castName,
174
+ boundsText: [boundsText, '…'].join(', '),
175
+ replacement: rewrite.replacement,
176
+ },
177
+ fix: (fixer) => [
178
+ fixer.replaceText(
179
+ node.callee,
180
+ `${arrLocalName}.${rewrite.replacement}`,
181
+ ),
182
+ ...dropBoundsFix(fixer, node, rewrite),
183
+ ],
184
+ });
185
+ },
186
+ };
187
+ },
188
+ defaultOptions: [],
189
+ } as const;
190
+
191
+ /** The `Arr.<cast>` method name of `node`, or `undefined` for other calls. */
192
+ const getCastName = (
193
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
194
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
195
+ node: TSESTree.CallExpression,
196
+ arrLocalName: string,
197
+ ): string | undefined => {
198
+ const { callee } = node;
199
+
200
+ return callee.type === AST_NODE_TYPES.MemberExpression &&
201
+ !callee.computed &&
202
+ callee.object.type === AST_NODE_TYPES.Identifier &&
203
+ callee.object.name === arrLocalName &&
204
+ callee.property.type === AST_NODE_TYPES.Identifier
205
+ ? callee.property.name
206
+ : undefined;
207
+ };
208
+
209
+ /**
210
+ * Whether the call is `cast(...bounds, array)` with every bound written as the
211
+ * exact numeric literal the rewrite requires.
212
+ *
213
+ * The arity check also excludes the curried form (`cast(...bounds)`):
214
+ * `Arr.asNonEmptyArray` has no curried counterpart, and a curried
215
+ * `asFixedLengthArray` would need the bound kept in a different position.
216
+ */
217
+ const matchesBounds = (
218
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
219
+ node: TSESTree.CallExpression,
220
+ rewrite: CastRewrite,
221
+ ): boolean => {
222
+ const args = node.arguments;
223
+
224
+ const boundCount = rewrite.bounds.length;
225
+
226
+ if (args.length !== boundCount + 1 || node.typeArguments !== undefined) {
227
+ return false;
228
+ }
229
+
230
+ if (args.some((arg) => arg.type === AST_NODE_TYPES.SpreadElement)) {
231
+ return false;
232
+ }
233
+
234
+ const bounds = args.slice(0, boundCount).map(getLengthLiteral);
235
+
236
+ const [first] = bounds;
237
+
238
+ return rewrite.bounds.every((matcher, index) => {
239
+ const value = bounds[index];
240
+
241
+ if (value === undefined) return false;
242
+
243
+ switch (matcher) {
244
+ case 'anyLength':
245
+ return true;
246
+
247
+ case 'sameAsFirst':
248
+ return value === first;
249
+
250
+ default:
251
+ return value === matcher;
252
+ }
253
+ });
254
+ };
255
+
256
+ /**
257
+ * Removes the length arguments the rewrite drops. Each removal runs from the
258
+ * dropped argument up to the start of the next one, so the comma, whitespace
259
+ * and any comments in between go with it; the ranges never overlap.
260
+ */
261
+ const dropBoundsFix = (
262
+ fixer: TSESLint.RuleFixer,
263
+ // AST nodes hold mutable child arrays, so they are not deeply readonly.
264
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
265
+ node: TSESTree.CallExpression,
266
+ rewrite: CastRewrite,
267
+ ): readonly TSESLint.RuleFix[] =>
268
+ rewrite.bounds.flatMap((_, index) => {
269
+ if (rewrite.keep.includes(index)) return [];
270
+
271
+ const dropped = node.arguments[index];
272
+
273
+ const next = node.arguments[index + 1];
274
+
275
+ return dropped === undefined || next === undefined
276
+ ? []
277
+ : [fixer.removeRange([dropped.range[0], next.range[0]])];
278
+ });
279
+
280
+ /** A non-negative integer literal's value, or `undefined` for anything else. */
281
+ const getLengthLiteral = (
282
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
283
+ node: TSESTree.Node,
284
+ ): number | undefined =>
285
+ node.type === AST_NODE_TYPES.Literal &&
286
+ typeof node.value === 'number' &&
287
+ Number.isSafeInteger(node.value) &&
288
+ node.value >= 0
289
+ ? node.value
290
+ : undefined;