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
@@ -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;
@@ -0,0 +1,305 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferCanonicalLengthCast } from './prefer-canonical-length-cast.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ },
13
+ },
14
+ });
15
+
16
+ describe('prefer-canonical-length-cast', () => {
17
+ tester.run('prefer-canonical-length-cast', preferCanonicalLengthCast, {
18
+ valid: [
19
+ {
20
+ name: 'ignores non-degenerate bounds',
21
+ code: dedent`
22
+ import { Arr } from 'ts-data-forge';
23
+
24
+ declare const xs: readonly number[];
25
+ const ys = Arr.asMinLengthArray(2, xs);
26
+ `,
27
+ },
28
+ {
29
+ name: 'ignores a bounded range that is not degenerate',
30
+ code: dedent`
31
+ import { Arr } from 'ts-data-forge';
32
+
33
+ declare const xs: readonly number[];
34
+ const ys = Arr.asBoundedLengthArray(1, 5, xs);
35
+ `,
36
+ },
37
+ {
38
+ name: 'ignores asMaxLengthArray with a bound other than 0',
39
+ code: dedent`
40
+ import { Arr } from 'ts-data-forge';
41
+
42
+ declare const xs: readonly number[];
43
+ const ys = Arr.asMaxLengthArray(5, xs);
44
+ `,
45
+ },
46
+ {
47
+ name: 'ignores the widening asBoundedLengthArray(0, n, xs) case',
48
+ code: dedent`
49
+ import { Arr } from 'ts-data-forge';
50
+
51
+ declare const xs: readonly number[];
52
+ const ys = Arr.asBoundedLengthArray(0, 5, xs);
53
+ `,
54
+ },
55
+ {
56
+ name: 'ignores the curried form',
57
+ code: dedent`
58
+ import { Arr } from 'ts-data-forge';
59
+
60
+ const asNonEmpty = Arr.asMinLengthArray(1);
61
+ const asPair = Arr.asBoundedLengthArray(2, 2);
62
+ `,
63
+ },
64
+ {
65
+ name: 'ignores a non-literal bound',
66
+ code: dedent`
67
+ import { Arr } from 'ts-data-forge';
68
+
69
+ declare const xs: readonly number[];
70
+ const n = 1;
71
+ const ys = Arr.asMinLengthArray(n, xs);
72
+ `,
73
+ },
74
+ {
75
+ name: 'ignores already-canonical casts',
76
+ code: dedent`
77
+ import { Arr } from 'ts-data-forge';
78
+
79
+ declare const xs: readonly number[];
80
+ const ys = Arr.asNonEmptyArray(xs);
81
+ `,
82
+ },
83
+ {
84
+ name: 'ignores an Arr that is not from ts-data-forge',
85
+ code: dedent`
86
+ import { Arr } from './my-helpers.mjs';
87
+
88
+ declare const xs: readonly number[];
89
+ const ys = Arr.asMinLengthArray(1, xs);
90
+ `,
91
+ },
92
+ {
93
+ name: 'ignores spread arguments',
94
+ code: dedent`
95
+ import { Arr } from 'ts-data-forge';
96
+
97
+ declare const args: readonly [1, readonly number[]];
98
+ const ys = Arr.asMinLengthArray(...args);
99
+ `,
100
+ },
101
+ ],
102
+ invalid: [
103
+ {
104
+ name: 'asMinLengthArray(1, xs) becomes asNonEmptyArray(xs)',
105
+ code: dedent`
106
+ import { Arr } from 'ts-data-forge';
107
+
108
+ declare const xs: readonly number[];
109
+ const ys = Arr.asMinLengthArray(1, xs);
110
+ `,
111
+ output: dedent`
112
+ import { Arr } from 'ts-data-forge';
113
+
114
+ declare const xs: readonly number[];
115
+ const ys = Arr.asNonEmptyArray(xs);
116
+ `,
117
+ errors: [{ messageId: 'useCanonicalCast' }],
118
+ },
119
+ {
120
+ name: 'asBoundedLengthArray(n, n, xs) becomes asFixedLengthArray(n, xs)',
121
+ code: dedent`
122
+ import { Arr } from 'ts-data-forge';
123
+
124
+ declare const xs: readonly number[];
125
+ const ys = Arr.asBoundedLengthArray(3, 3, xs);
126
+ `,
127
+ output: dedent`
128
+ import { Arr } from 'ts-data-forge';
129
+
130
+ declare const xs: readonly number[];
131
+ const ys = Arr.asFixedLengthArray(3, xs);
132
+ `,
133
+ errors: [{ messageId: 'useCanonicalCast' }],
134
+ },
135
+ {
136
+ name: 'asMaxLengthArray(0, xs) becomes asEmptyArray(xs)',
137
+ code: dedent`
138
+ import { Arr } from 'ts-data-forge';
139
+
140
+ declare const xs: readonly number[];
141
+ const ys = Arr.asMaxLengthArray(0, xs);
142
+ `,
143
+ output: dedent`
144
+ import { Arr } from 'ts-data-forge';
145
+
146
+ declare const xs: readonly number[];
147
+ const ys = Arr.asEmptyArray(xs);
148
+ `,
149
+ errors: [{ messageId: 'useCanonicalCast' }],
150
+ },
151
+ {
152
+ name: 'keeps a complex array expression intact',
153
+ code: dedent`
154
+ import { Arr } from 'ts-data-forge';
155
+
156
+ declare const obj: Readonly<{ items: readonly number[] }>;
157
+ const ys = Arr.asMinLengthArray(1, obj.items.filter((x) => x > 0));
158
+ `,
159
+ output: dedent`
160
+ import { Arr } from 'ts-data-forge';
161
+
162
+ declare const obj: Readonly<{ items: readonly number[] }>;
163
+ const ys = Arr.asNonEmptyArray(obj.items.filter((x) => x > 0));
164
+ `,
165
+ errors: [{ messageId: 'useCanonicalCast' }],
166
+ },
167
+ {
168
+ name: 'resolves an aliased Arr import',
169
+ code: dedent`
170
+ import { Arr as A } from 'ts-data-forge';
171
+
172
+ declare const xs: readonly number[];
173
+ const ys = A.asMinLengthArray(1, xs);
174
+ `,
175
+ output: dedent`
176
+ import { Arr as A } from 'ts-data-forge';
177
+
178
+ declare const xs: readonly number[];
179
+ const ys = A.asNonEmptyArray(xs);
180
+ `,
181
+ errors: [{ messageId: 'useCanonicalCast' }],
182
+ },
183
+ {
184
+ name: 'rewrites several casts in one file',
185
+ code: dedent`
186
+ import { Arr } from 'ts-data-forge';
187
+
188
+ declare const xs: readonly number[];
189
+ const a = Arr.asMinLengthArray(1, xs);
190
+ const b = Arr.asBoundedLengthArray(2, 2, xs);
191
+ const c = Arr.asMaxLengthArray(0, xs);
192
+ `,
193
+ output: dedent`
194
+ import { Arr } from 'ts-data-forge';
195
+
196
+ declare const xs: readonly number[];
197
+ const a = Arr.asNonEmptyArray(xs);
198
+ const b = Arr.asFixedLengthArray(2, xs);
199
+ const c = Arr.asEmptyArray(xs);
200
+ `,
201
+ errors: [
202
+ { messageId: 'useCanonicalCast' },
203
+ { messageId: 'useCanonicalCast' },
204
+ { messageId: 'useCanonicalCast' },
205
+ ],
206
+ },
207
+ {
208
+ name: 'asFixedLengthArray(0, xs) becomes asEmptyArray(xs)',
209
+ code: dedent`
210
+ import { Arr } from 'ts-data-forge';
211
+
212
+ declare const xs: readonly number[];
213
+ const ys = Arr.asFixedLengthArray(0, xs);
214
+ `,
215
+ output: dedent`
216
+ import { Arr } from 'ts-data-forge';
217
+
218
+ declare const xs: readonly number[];
219
+ const ys = Arr.asEmptyArray(xs);
220
+ `,
221
+ errors: [{ messageId: 'useCanonicalCast' }],
222
+ },
223
+ {
224
+ name: 'asBoundedLengthArray(0, 0, xs) becomes asEmptyArray(xs)',
225
+ code: dedent`
226
+ import { Arr } from 'ts-data-forge';
227
+
228
+ declare const xs: readonly number[];
229
+ const ys = Arr.asBoundedLengthArray(0, 0, xs);
230
+ `,
231
+ output: dedent`
232
+ import { Arr } from 'ts-data-forge';
233
+
234
+ declare const xs: readonly number[];
235
+ const ys = Arr.asEmptyArray(xs);
236
+ `,
237
+ errors: [{ messageId: 'useCanonicalCast' }],
238
+ },
239
+ {
240
+ name: 'asMinLengthTuple(1, xs) becomes asNonEmptyTuple(xs)',
241
+ code: dedent`
242
+ import { Arr } from 'ts-data-forge';
243
+
244
+ declare const xs: readonly number[];
245
+ const ys = Arr.asMinLengthTuple(1, xs);
246
+ `,
247
+ output: dedent`
248
+ import { Arr } from 'ts-data-forge';
249
+
250
+ declare const xs: readonly number[];
251
+ const ys = Arr.asNonEmptyTuple(xs);
252
+ `,
253
+ errors: [{ messageId: 'useCanonicalCast' }],
254
+ },
255
+ {
256
+ name: 'asFixedLengthTuple(0, xs) becomes asEmptyTuple(xs)',
257
+ code: dedent`
258
+ import { Arr } from 'ts-data-forge';
259
+
260
+ declare const xs: readonly number[];
261
+ const ys = Arr.asFixedLengthTuple(0, xs);
262
+ `,
263
+ output: dedent`
264
+ import { Arr } from 'ts-data-forge';
265
+
266
+ declare const xs: readonly number[];
267
+ const ys = Arr.asEmptyTuple(xs);
268
+ `,
269
+ errors: [{ messageId: 'useCanonicalCast' }],
270
+ },
271
+ {
272
+ name: 'asMaxLengthTuple(0, xs) becomes asEmptyTuple(xs)',
273
+ code: dedent`
274
+ import { Arr } from 'ts-data-forge';
275
+
276
+ declare const xs: readonly number[];
277
+ const ys = Arr.asMaxLengthTuple(0, xs);
278
+ `,
279
+ output: dedent`
280
+ import { Arr } from 'ts-data-forge';
281
+
282
+ declare const xs: readonly number[];
283
+ const ys = Arr.asEmptyTuple(xs);
284
+ `,
285
+ errors: [{ messageId: 'useCanonicalCast' }],
286
+ },
287
+ {
288
+ name: 'asBoundedLengthTuple(n, n, xs) becomes asFixedLengthTuple(n, xs)',
289
+ code: dedent`
290
+ import { Arr } from 'ts-data-forge';
291
+
292
+ declare const xs: readonly number[];
293
+ const ys = Arr.asBoundedLengthTuple(3, 3, xs);
294
+ `,
295
+ output: dedent`
296
+ import { Arr } from 'ts-data-forge';
297
+
298
+ declare const xs: readonly number[];
299
+ const ys = Arr.asFixedLengthTuple(3, xs);
300
+ `,
301
+ errors: [{ messageId: 'useCanonicalCast' }],
302
+ },
303
+ ],
304
+ });
305
+ });