eslint-plugin-ts-data-forge 0.2.0 → 0.3.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 (52) hide show
  1. package/README.md +16 -14
  2. package/dist/plugin.d.mts +2 -0
  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/prefer-arr-is-bounded-length-array.mjs +1 -1
  10. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  11. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
  12. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  13. package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
  14. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  15. package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
  16. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-sum.mjs +2 -2
  18. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  19. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  20. package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
  21. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
  22. package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
  23. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
  24. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  25. package/dist/rules/prefer-canonical-length-guard.mjs +12 -8
  26. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  27. package/dist/rules/prefer-range-for-loop.mjs +1 -1
  28. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  29. package/dist/rules/rules.d.mts +1 -0
  30. package/dist/rules/rules.d.mts.map +1 -1
  31. package/dist/rules/rules.mjs +2 -0
  32. package/dist/rules/rules.mjs.map +1 -1
  33. package/package.json +3 -3
  34. package/src/plugin.mts +1 -0
  35. package/src/rule-types.mts +17 -1
  36. package/src/rules/import-utils.mts +0 -1
  37. package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
  38. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
  39. package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
  40. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
  41. package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
  42. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
  43. package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
  44. package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
  45. package/src/rules/prefer-arr-sum.mts +2 -2
  46. package/src/rules/prefer-canonical-array-slicing.mts +0 -2
  47. package/src/rules/prefer-canonical-length-cast.mts +290 -0
  48. package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
  49. package/src/rules/prefer-canonical-length-guard.mts +40 -21
  50. package/src/rules/prefer-canonical-length-guard.test.mts +32 -32
  51. package/src/rules/prefer-range-for-loop.mts +1 -1
  52. package/src/rules/rules.mts +2 -0
@@ -52,28 +52,44 @@ const MESSAGES = {
52
52
  } as const satisfies Record<MessageIds, string>;
53
53
 
54
54
  /**
55
- * Length guards whose bound makes them redundant with `Arr.isEmpty` /
56
- * `Arr.isNonEmpty`, keyed by guard name.
55
+ * Length guards whose bound makes them redundant with a named degenerate
56
+ * guard, keyed by guard name.
57
57
  *
58
- * `isEmpty` narrows to `FixedLengthArray<0, E> & Xs` and `isNonEmpty` to
59
- * `MinLengthArray<1, E> & Xs`, so:
58
+ * Every entry stays inside its own family rather than crossing the
59
+ * branded/structural divide:
60
60
  *
61
- * - The `*Array` entries are **type-identical** — `isFixedLengthArray(xs, 0)`
62
- * and `isMinLengthArray(xs, 1)` produce exactly those types.
63
- * - The `*Tuple` entries **narrow**: they resolve to the structural
64
- * `readonly []` / `readonly [E, ...E[]]`, and the canonical guards add the
65
- * brand on top. The result stays assignable everywhere the old type was, so
66
- * the rewrite is safe, but it is a strengthening rather than a pure rename.
61
+ * - the branded `*Array` guards go to `isEmpty` / `isNonEmpty`, which narrow to
62
+ * `FixedLengthArray<0, E> & Xs` / `MinLengthArray<1, E> & Xs`;
63
+ * - the structural `*Tuple` guards go to `isEmptyTuple` / `isNonEmptyTuple`,
64
+ * which narrow to `readonly []` / `MinLengthTuple<1, E>`.
65
+ *
66
+ * Most entries are then **type-identical** — `isFixedLengthArray(0, xs)` and
67
+ * `isMinLengthArray(1, xs)` produce exactly what `isEmpty` / `isNonEmpty`
68
+ * narrow to, and `FixedLengthTuple<0, E>`, `MaxLengthTuple<0, E>` and
69
+ * `BoundedLengthTuple<0, 0, E>` are all `readonly []`. Two *strengthen*:
70
+ * `MaxLengthArray<0, E>` and `BoundedLengthArray<0, 0, E>` do not include the
71
+ * `readonly []` structural part that `FixedLengthArray<0, E>` intersects on, so
72
+ * rewriting them to `isEmpty` adds it. That is sound for a guard — the narrowed
73
+ * type only flows outwards, and the stronger type is assignable everywhere the
74
+ * old one was — and it stays within the branded family.
75
+ *
76
+ * Sending a `*Tuple` guard to the branded `isEmpty` would also be sound, but it
77
+ * would silently change which family the value belongs to, which is why each
78
+ * family keeps its own lane.
67
79
  */
68
80
  const GUARD_REWRITES = [
69
81
  { guard: 'isFixedLengthArray', bounds: [0], replacement: 'isEmpty' },
70
- { guard: 'isFixedLengthTuple', bounds: [0], replacement: 'isEmpty' },
71
- { guard: 'isMaxLengthTuple', bounds: [0], replacement: 'isEmpty' },
72
82
  { guard: 'isMaxLengthArray', bounds: [0], replacement: 'isEmpty' },
73
- { guard: 'isBoundedLengthTuple', bounds: [0, 0], replacement: 'isEmpty' },
74
83
  { guard: 'isBoundedLengthArray', bounds: [0, 0], replacement: 'isEmpty' },
75
84
  { guard: 'isMinLengthArray', bounds: [1], replacement: 'isNonEmpty' },
76
- { guard: 'isMinLengthTuple', bounds: [1], replacement: 'isNonEmpty' },
85
+ { guard: 'isFixedLengthTuple', bounds: [0], replacement: 'isEmptyTuple' },
86
+ { guard: 'isMaxLengthTuple', bounds: [0], replacement: 'isEmptyTuple' },
87
+ {
88
+ guard: 'isBoundedLengthTuple',
89
+ bounds: [0, 0],
90
+ replacement: 'isEmptyTuple',
91
+ },
92
+ { guard: 'isMinLengthTuple', bounds: [1], replacement: 'isNonEmptyTuple' },
77
93
  ] as const satisfies readonly Readonly<{
78
94
  guard: string;
79
95
  bounds: readonly number[];
@@ -88,7 +104,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
88
104
  type: 'suggestion',
89
105
  docs: {
90
106
  description:
91
- 'Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and degenerate guards (e.g. `Arr.isFixedLengthTuple(xs, 0)`) become `Arr.isEmpty` / `Arr.isNonEmpty`.',
107
+ 'Normalize array-length checks to their canonical `Arr` guard: `xs.length <op> n` becomes the matching `Arr.is*` guard, and a degenerate guard becomes the named one for its family (`Arr.isFixedLengthArray(0, xs)` → `Arr.isEmpty`, `Arr.isFixedLengthTuple(0, xs)` → `Arr.isEmptyTuple`).',
92
108
  },
93
109
  fixable: 'code',
94
110
  schema: [],
@@ -125,7 +141,8 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
125
141
  return;
126
142
  }
127
143
 
128
- const [array] = node.arguments;
144
+ // The array is the last argument; the bounds precede it.
145
+ const array = node.arguments[rewrite.bounds.length];
129
146
 
130
147
  if (array === undefined) return;
131
148
 
@@ -137,7 +154,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
137
154
  data: {
138
155
  arrName: arrLocalName,
139
156
  guard: guardName,
140
- boundsText: ['…', ...rewrite.bounds.map(String)].join(', '),
157
+ boundsText: [...rewrite.bounds.map(String), '…'].join(', '),
141
158
  replacement: rewrite.replacement,
142
159
  },
143
160
  fix: (fixer) =>
@@ -207,8 +224,11 @@ const getGuardName = (
207
224
  };
208
225
 
209
226
  /**
210
- * Whether the call is `guard(array, ...bounds)` with every bound written as the
227
+ * Whether the call is `guard(...bounds, array)` with every bound written as the
211
228
  * exact numeric literal the rewrite requires.
229
+ *
230
+ * The arity check also excludes the curried form (`guard(...bounds)`): the
231
+ * degenerate guards take only the array, so there is nothing to rewrite it to.
212
232
  */
213
233
  const matchesBounds = (
214
234
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -224,8 +244,7 @@ const matchesBounds = (
224
244
  return args.every(
225
245
  (arg, index) =>
226
246
  arg.type !== AST_NODE_TYPES.SpreadElement &&
227
- (index === 0 ||
228
- (arg.type === AST_NODE_TYPES.Literal &&
229
- arg.value === bounds[index - 1])),
247
+ (index === bounds.length ||
248
+ (arg.type === AST_NODE_TYPES.Literal && arg.value === bounds[index])),
230
249
  );
231
250
  };
@@ -55,7 +55,7 @@ describe('prefer-canonical-length-guard (folded-in length comparisons)', () => {
55
55
  errors: [{ messageId: 'useIsNonEmpty' }],
56
56
  },
57
57
  {
58
- name: 'xs.length >= 2 becomes Arr.isMinLengthArray(xs, 2)',
58
+ name: 'xs.length >= 2 becomes Arr.isMinLengthArray(2, xs)',
59
59
  code: dedent`
60
60
  const xs = [1, 2, 3];
61
61
  const ok = xs.length >= 2;
@@ -63,12 +63,12 @@ describe('prefer-canonical-length-guard (folded-in length comparisons)', () => {
63
63
  output: dedent`
64
64
  import { Arr } from 'ts-data-forge';
65
65
  const xs = [1, 2, 3];
66
- const ok = Arr.isMinLengthArray(xs, 2);
66
+ const ok = Arr.isMinLengthArray(2, xs);
67
67
  `,
68
68
  errors: [{ messageId: 'useIsMinLengthArray' }],
69
69
  },
70
70
  {
71
- name: 'xs.length === 3 becomes Arr.isFixedLengthArray(xs, 3)',
71
+ name: 'xs.length === 3 becomes Arr.isFixedLengthArray(3, xs)',
72
72
  code: dedent`
73
73
  const xs = [1, 2, 3];
74
74
  const ok = xs.length === 3;
@@ -76,7 +76,7 @@ describe('prefer-canonical-length-guard (folded-in length comparisons)', () => {
76
76
  output: dedent`
77
77
  import { Arr } from 'ts-data-forge';
78
78
  const xs = [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
  },
@@ -93,7 +93,7 @@ describe('prefer-canonical-length-guard', () => {
93
93
  import { Arr } from 'ts-data-forge';
94
94
 
95
95
  declare const xs: readonly number[];
96
- const ok = Arr.isFixedLengthTuple(xs, 2);
96
+ const ok = Arr.isFixedLengthTuple(2, xs);
97
97
  `,
98
98
  },
99
99
  {
@@ -102,7 +102,7 @@ describe('prefer-canonical-length-guard', () => {
102
102
  import { Arr } from 'ts-data-forge';
103
103
 
104
104
  declare const xs: readonly number[];
105
- const ok = Arr.isMinLengthArray(xs, 2);
105
+ const ok = Arr.isMinLengthArray(2, xs);
106
106
  `,
107
107
  },
108
108
  {
@@ -111,7 +111,7 @@ describe('prefer-canonical-length-guard', () => {
111
111
  import { Arr } from 'ts-data-forge';
112
112
 
113
113
  declare const xs: readonly number[];
114
- const ok = Arr.isBoundedLengthTuple(xs, 0, 1);
114
+ const ok = Arr.isBoundedLengthTuple(0, 1, xs);
115
115
  `,
116
116
  },
117
117
  {
@@ -121,7 +121,7 @@ describe('prefer-canonical-length-guard', () => {
121
121
 
122
122
  declare const xs: readonly number[];
123
123
  const n = 0;
124
- const ok = Arr.isFixedLengthTuple(xs, n);
124
+ const ok = Arr.isFixedLengthTuple(n, xs);
125
125
  `,
126
126
  },
127
127
  {
@@ -130,7 +130,7 @@ describe('prefer-canonical-length-guard', () => {
130
130
  import { Arr } from './my-helpers.mjs';
131
131
 
132
132
  declare const xs: readonly number[];
133
- const ok = Arr.isFixedLengthTuple(xs, 0);
133
+ const ok = Arr.isFixedLengthTuple(0, xs);
134
134
  `,
135
135
  },
136
136
  {
@@ -154,12 +154,12 @@ describe('prefer-canonical-length-guard', () => {
154
154
  ],
155
155
  invalid: [
156
156
  {
157
- name: 'isFixedLengthArray(xs, 0) becomes isEmpty(xs)',
157
+ name: 'isFixedLengthArray(0, xs) becomes isEmpty(xs)',
158
158
  code: dedent`
159
159
  import { Arr } from 'ts-data-forge';
160
160
 
161
161
  declare const xs: readonly number[];
162
- const ok = Arr.isFixedLengthArray(xs, 0);
162
+ const ok = Arr.isFixedLengthArray(0, xs);
163
163
  `,
164
164
  output: dedent`
165
165
  import { Arr } from 'ts-data-forge';
@@ -170,76 +170,76 @@ describe('prefer-canonical-length-guard', () => {
170
170
  errors: [{ messageId: 'useCanonicalGuard' }],
171
171
  },
172
172
  {
173
- name: 'isMinLengthTuple(xs, 1) becomes isNonEmpty(xs)',
173
+ name: 'isMinLengthTuple(1, xs) becomes isNonEmptyTuple(xs)',
174
174
  code: dedent`
175
175
  import { Arr } from 'ts-data-forge';
176
176
 
177
177
  declare const xs: readonly number[];
178
- const ok = Arr.isMinLengthTuple(xs, 1);
178
+ const ok = Arr.isMinLengthTuple(1, xs);
179
179
  `,
180
180
  output: dedent`
181
181
  import { Arr } from 'ts-data-forge';
182
182
 
183
183
  declare const xs: readonly number[];
184
- const ok = Arr.isNonEmpty(xs);
184
+ const ok = Arr.isNonEmptyTuple(xs);
185
185
  `,
186
186
  errors: [{ messageId: 'useCanonicalGuard' }],
187
187
  },
188
188
  {
189
- name: 'isFixedLengthTuple(xs, 0) becomes isEmpty(xs)',
189
+ name: 'isFixedLengthTuple(0, xs) becomes isEmptyTuple(xs)',
190
190
  code: dedent`
191
191
  import { Arr } from 'ts-data-forge';
192
192
 
193
193
  declare const xs: readonly number[];
194
- const ok = Arr.isFixedLengthTuple(xs, 0);
194
+ const ok = Arr.isFixedLengthTuple(0, xs);
195
195
  `,
196
196
  output: dedent`
197
197
  import { Arr } from 'ts-data-forge';
198
198
 
199
199
  declare const xs: readonly number[];
200
- const ok = Arr.isEmpty(xs);
200
+ const ok = Arr.isEmptyTuple(xs);
201
201
  `,
202
202
  errors: [{ messageId: 'useCanonicalGuard' }],
203
203
  },
204
204
  {
205
- name: 'isMaxLengthTuple(xs, 0) becomes isEmpty(xs)',
205
+ name: 'isMaxLengthTuple(0, xs) becomes isEmptyTuple(xs)',
206
206
  code: dedent`
207
207
  import { Arr } from 'ts-data-forge';
208
208
 
209
209
  declare const xs: readonly number[];
210
- const ok = Arr.isMaxLengthTuple(xs, 0);
210
+ const ok = Arr.isMaxLengthTuple(0, xs);
211
211
  `,
212
212
  output: dedent`
213
213
  import { Arr } from 'ts-data-forge';
214
214
 
215
215
  declare const xs: readonly number[];
216
- const ok = Arr.isEmpty(xs);
216
+ const ok = Arr.isEmptyTuple(xs);
217
217
  `,
218
218
  errors: [{ messageId: 'useCanonicalGuard' }],
219
219
  },
220
220
  {
221
- name: 'isBoundedLengthTuple(xs, 0, 0) becomes isEmpty(xs)',
221
+ name: 'isBoundedLengthTuple(0, 0, xs) becomes isEmptyTuple(xs)',
222
222
  code: dedent`
223
223
  import { Arr } from 'ts-data-forge';
224
224
 
225
225
  declare const xs: readonly number[];
226
- const ok = Arr.isBoundedLengthTuple(xs, 0, 0);
226
+ const ok = Arr.isBoundedLengthTuple(0, 0, xs);
227
227
  `,
228
228
  output: dedent`
229
229
  import { Arr } from 'ts-data-forge';
230
230
 
231
231
  declare const xs: readonly number[];
232
- const ok = Arr.isEmpty(xs);
232
+ const ok = Arr.isEmptyTuple(xs);
233
233
  `,
234
234
  errors: [{ messageId: 'useCanonicalGuard' }],
235
235
  },
236
236
  {
237
- name: 'isMinLengthArray(xs, 1) becomes isNonEmpty(xs)',
237
+ name: 'isMinLengthArray(1, xs) becomes isNonEmpty(xs)',
238
238
  code: dedent`
239
239
  import { Arr } from 'ts-data-forge';
240
240
 
241
241
  declare const xs: readonly number[];
242
- const ok = Arr.isMinLengthArray(xs, 1);
242
+ const ok = Arr.isMinLengthArray(1, xs);
243
243
  `,
244
244
  output: dedent`
245
245
  import { Arr } from 'ts-data-forge';
@@ -255,13 +255,13 @@ describe('prefer-canonical-length-guard', () => {
255
255
  import { Arr } from 'ts-data-forge';
256
256
 
257
257
  declare const obj: Readonly<{ items: readonly number[] }>;
258
- const ok = Arr.isFixedLengthTuple(obj.items.filter((x) => x > 0), 0);
258
+ const ok = Arr.isFixedLengthTuple(0, obj.items.filter((x) => x > 0));
259
259
  `,
260
260
  output: dedent`
261
261
  import { Arr } from 'ts-data-forge';
262
262
 
263
263
  declare const obj: Readonly<{ items: readonly number[] }>;
264
- const ok = Arr.isEmpty(obj.items.filter((x) => x > 0));
264
+ const ok = Arr.isEmptyTuple(obj.items.filter((x) => x > 0));
265
265
  `,
266
266
  errors: [{ messageId: 'useCanonicalGuard' }],
267
267
  },
@@ -271,13 +271,13 @@ describe('prefer-canonical-length-guard', () => {
271
271
  import { Arr as A } from 'ts-data-forge';
272
272
 
273
273
  declare const xs: readonly number[];
274
- const ok = A.isFixedLengthTuple(xs, 0);
274
+ const ok = A.isFixedLengthTuple(0, xs);
275
275
  `,
276
276
  output: dedent`
277
277
  import { Arr as A } from 'ts-data-forge';
278
278
 
279
279
  declare const xs: readonly number[];
280
- const ok = A.isEmpty(xs);
280
+ const ok = A.isEmptyTuple(xs);
281
281
  `,
282
282
  errors: [{ messageId: 'useCanonicalGuard' }],
283
283
  },
@@ -287,14 +287,14 @@ describe('prefer-canonical-length-guard', () => {
287
287
  import { Arr } from 'ts-data-forge';
288
288
 
289
289
  declare const xs: readonly number[];
290
- const a = Arr.isFixedLengthTuple(xs, 0);
291
- const b = Arr.isMinLengthArray(xs, 1);
290
+ const a = Arr.isFixedLengthTuple(0, xs);
291
+ const b = Arr.isMinLengthArray(1, xs);
292
292
  `,
293
293
  output: dedent`
294
294
  import { Arr } from 'ts-data-forge';
295
295
 
296
296
  declare const xs: readonly number[];
297
- const a = Arr.isEmpty(xs);
297
+ const a = Arr.isEmptyTuple(xs);
298
298
  const b = Arr.isNonEmpty(xs);
299
299
  `,
300
300
  errors: [
@@ -50,7 +50,7 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
50
50
  if (
51
51
  node.init?.type !== AST_NODE_TYPES.VariableDeclaration ||
52
52
  node.init.kind !== 'let' ||
53
- !Arr.isFixedLengthTuple(node.init.declarations, 1)
53
+ !Arr.isFixedLengthTuple(1, node.init.declarations)
54
54
  ) {
55
55
  return;
56
56
  }
@@ -4,6 +4,7 @@ import { preferArrIsArray } from './prefer-arr-is-array.mjs';
4
4
  import { preferArrSum } from './prefer-arr-sum.mjs';
5
5
  import { preferAsInt } from './prefer-as-int.mjs';
6
6
  import { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';
7
+ import { preferCanonicalLengthCast } from './prefer-canonical-length-cast.mjs';
7
8
  import { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';
8
9
  import { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';
9
10
  import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
@@ -15,6 +16,7 @@ import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
15
16
  export const tsDataForgeRules = {
16
17
  'prefer-canonical-array-slicing': preferCanonicalArraySlicing,
17
18
  'prefer-canonical-length-guard': preferCanonicalLengthGuard,
19
+ 'prefer-canonical-length-cast': preferCanonicalLengthCast,
18
20
  'prefer-arr-is-array': preferArrIsArray,
19
21
  'prefer-arr-sum': preferArrSum,
20
22
  'prefer-as-int': preferAsInt,