eslint-plugin-ts-data-forge 0.1.7 → 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 (60) hide show
  1. package/README.md +49 -19
  2. package/dist/index.d.mts +1 -1
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/plugin.d.mts +46 -1
  5. package/dist/plugin.d.mts.map +1 -1
  6. package/dist/plugin.mjs +35 -1
  7. package/dist/plugin.mjs.map +1 -1
  8. package/dist/rule-types.d.mts +6 -51
  9. package/dist/rule-types.d.mts.map +1 -1
  10. package/dist/rules/import-utils.mjs.map +1 -1
  11. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +1 -1
  12. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  13. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
  14. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  15. package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
  16. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
  18. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  19. package/dist/rules/prefer-arr-sum.mjs +2 -2
  20. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  21. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  22. package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
  23. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
  24. package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
  25. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
  26. package/dist/rules/prefer-canonical-length-guard.d.mts +6 -0
  27. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -0
  28. package/dist/rules/prefer-canonical-length-guard.mjs +129 -0
  29. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -0
  30. package/dist/rules/prefer-range-for-loop.mjs +1 -1
  31. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  32. package/dist/rules/rules.d.mts +2 -5
  33. package/dist/rules/rules.d.mts.map +1 -1
  34. package/dist/rules/rules.mjs +4 -10
  35. package/dist/rules/rules.mjs.map +1 -1
  36. package/dist/types.d.mts +1 -0
  37. package/dist/types.d.mts.map +1 -1
  38. package/package.json +7 -3
  39. package/src/index.mts +1 -1
  40. package/src/plugin.mts +47 -3
  41. package/src/plugin.test.mts +24 -0
  42. package/src/rule-types.mts +6 -54
  43. package/src/rules/import-utils.mts +0 -1
  44. package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
  45. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
  46. package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
  47. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
  48. package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
  49. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
  50. package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
  51. package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
  52. package/src/rules/prefer-arr-sum.mts +2 -2
  53. package/src/rules/prefer-canonical-array-slicing.mts +0 -2
  54. package/src/rules/prefer-canonical-length-cast.mts +290 -0
  55. package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
  56. package/src/rules/prefer-canonical-length-guard.mts +250 -0
  57. package/src/rules/prefer-canonical-length-guard.test.mts +307 -0
  58. package/src/rules/prefer-range-for-loop.mts +1 -1
  59. package/src/rules/rules.mts +4 -10
  60. package/src/types.mts +2 -0
@@ -0,0 +1,250 @@
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
+ import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
8
+ import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
9
+ import { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';
10
+ import { preferArrIsMinLengthArray } from './prefer-arr-is-min-length-array.mjs';
11
+ import { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';
12
+
13
+ type Options = readonly [];
14
+
15
+ type MessageIds =
16
+ | 'useCanonicalGuard'
17
+ | 'useIsBoundedLengthArray'
18
+ | 'useIsFixedLengthArray'
19
+ | 'useIsMaxLengthArray'
20
+ | 'useIsMinLengthArray'
21
+ | 'useIsNonEmpty';
22
+
23
+ /**
24
+ * The `xs.length <op> n` → `Arr.is*` rules folded into this one.
25
+ *
26
+ * Their implementations are reused as-is rather than rewritten: this rule
27
+ * merges their visitors, so every case they already cover (type-aware array
28
+ * checks, `Arr` import insertion, the `&&`-bounded-range special case) keeps
29
+ * behaving exactly as before, and their co-located tests keep exercising them
30
+ * directly.
31
+ */
32
+ const COMPARISON_RULES = [
33
+ preferArrIsNonEmpty,
34
+ preferArrIsMinLengthArray,
35
+ preferArrIsMaxLengthArray,
36
+ preferArrIsBoundedLengthArray,
37
+ preferArrIsFixedLengthArray,
38
+ ] as const;
39
+
40
+ const MESSAGES = {
41
+ useCanonicalGuard:
42
+ 'Replace `{{arrName}}.{{guard}}({{boundsText}})` with `{{arrName}}.{{replacement}}(...)`: the bound makes the two guards equivalent.',
43
+ useIsNonEmpty: preferArrIsNonEmpty.meta.messages.useIsNonEmpty,
44
+ useIsMinLengthArray:
45
+ preferArrIsMinLengthArray.meta.messages.useIsMinLengthArray,
46
+ useIsMaxLengthArray:
47
+ preferArrIsMaxLengthArray.meta.messages.useIsMaxLengthArray,
48
+ useIsBoundedLengthArray:
49
+ preferArrIsBoundedLengthArray.meta.messages.useIsBoundedLengthArray,
50
+ useIsFixedLengthArray:
51
+ preferArrIsFixedLengthArray.meta.messages.useIsFixedLengthArray,
52
+ } as const satisfies Record<MessageIds, string>;
53
+
54
+ /**
55
+ * Length guards whose bound makes them redundant with a named degenerate
56
+ * guard, keyed by guard name.
57
+ *
58
+ * Every entry stays inside its own family rather than crossing the
59
+ * branded/structural divide:
60
+ *
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.
79
+ */
80
+ const GUARD_REWRITES = [
81
+ { guard: 'isFixedLengthArray', bounds: [0], replacement: 'isEmpty' },
82
+ { guard: 'isMaxLengthArray', bounds: [0], replacement: 'isEmpty' },
83
+ { guard: 'isBoundedLengthArray', bounds: [0, 0], replacement: 'isEmpty' },
84
+ { guard: 'isMinLengthArray', 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' },
93
+ ] as const satisfies readonly Readonly<{
94
+ guard: string;
95
+ bounds: readonly number[];
96
+ replacement: string;
97
+ }>[];
98
+
99
+ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
100
+ MessageIds,
101
+ Options
102
+ > = {
103
+ meta: {
104
+ type: 'suggestion',
105
+ docs: {
106
+ description:
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`).',
108
+ },
109
+ fixable: 'code',
110
+ schema: [],
111
+ messages: MESSAGES,
112
+ },
113
+
114
+ create: (context) => {
115
+ const sourceCode = context.sourceCode;
116
+
117
+ const arrLocalName = getImportedLocalName(
118
+ getTsDataForgeImport(sourceCode.ast),
119
+ 'Arr',
120
+ );
121
+
122
+ const comparisonVisitors = COMPARISON_RULES.map((rule) =>
123
+ rule.create(context),
124
+ );
125
+
126
+ if (arrLocalName === undefined) return mergeVisitors(comparisonVisitors);
127
+
128
+ return mergeVisitors([
129
+ ...comparisonVisitors,
130
+ {
131
+ CallExpression: (node) => {
132
+ const guardName = getGuardName(node, arrLocalName);
133
+
134
+ if (guardName === undefined) return;
135
+
136
+ const rewrite = GUARD_REWRITES.find(
137
+ (entry) => entry.guard === guardName,
138
+ );
139
+
140
+ if (rewrite === undefined || !matchesBounds(node, rewrite.bounds)) {
141
+ return;
142
+ }
143
+
144
+ // The array is the last argument; the bounds precede it.
145
+ const array = node.arguments[rewrite.bounds.length];
146
+
147
+ if (array === undefined) return;
148
+
149
+ const arrayText = sourceCode.getText(array);
150
+
151
+ context.report({
152
+ node,
153
+ messageId: 'useCanonicalGuard',
154
+ data: {
155
+ arrName: arrLocalName,
156
+ guard: guardName,
157
+ boundsText: [...rewrite.bounds.map(String), '…'].join(', '),
158
+ replacement: rewrite.replacement,
159
+ },
160
+ fix: (fixer) =>
161
+ fixer.replaceText(
162
+ node,
163
+ `${arrLocalName}.${rewrite.replacement}(${arrayText})`,
164
+ ),
165
+ });
166
+ },
167
+ },
168
+ ]);
169
+ },
170
+ defaultOptions: [],
171
+ } as const;
172
+
173
+ /**
174
+ * Combines several rule listeners into one, calling every handler registered
175
+ * for a given selector in order.
176
+ */
177
+ const mergeVisitors = (
178
+ // `RuleListener` holds mutable AST handler signatures, so it is not deeply
179
+ // readonly.
180
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
181
+ visitors: readonly TSESLint.RuleListener[],
182
+ ): TSESLint.RuleListener => {
183
+ const mut_merged: Record<string, ((node: never) => void)[]> = {};
184
+
185
+ for (const visitor of visitors) {
186
+ for (const [selector, handler] of Object.entries(visitor)) {
187
+ if (typeof handler !== 'function') continue;
188
+
189
+ const mut_handlers = mut_merged[selector] ?? [];
190
+
191
+ mut_handlers.push(handler);
192
+
193
+ mut_merged[selector] = mut_handlers;
194
+ }
195
+ }
196
+
197
+ return Object.fromEntries(
198
+ Object.entries(mut_merged).map(([selector, handlers]) => [
199
+ selector,
200
+ (node: never) => {
201
+ for (const handler of handlers) {
202
+ handler(node);
203
+ }
204
+ },
205
+ ]),
206
+ );
207
+ };
208
+
209
+ /** The `Arr.<guard>` method name of `node`, or `undefined` for other calls. */
210
+ const getGuardName = (
211
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
212
+ node: TSESTree.CallExpression,
213
+ arrLocalName: string,
214
+ ): string | undefined => {
215
+ const { callee } = node;
216
+
217
+ return callee.type === AST_NODE_TYPES.MemberExpression &&
218
+ !callee.computed &&
219
+ callee.object.type === AST_NODE_TYPES.Identifier &&
220
+ callee.object.name === arrLocalName &&
221
+ callee.property.type === AST_NODE_TYPES.Identifier
222
+ ? callee.property.name
223
+ : undefined;
224
+ };
225
+
226
+ /**
227
+ * Whether the call is `guard(...bounds, array)` with every bound written as the
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.
232
+ */
233
+ const matchesBounds = (
234
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
235
+ node: TSESTree.CallExpression,
236
+ bounds: readonly number[],
237
+ ): boolean => {
238
+ const args = node.arguments;
239
+
240
+ if (args.length !== bounds.length + 1 || node.typeArguments !== undefined) {
241
+ return false;
242
+ }
243
+
244
+ return args.every(
245
+ (arg, index) =>
246
+ arg.type !== AST_NODE_TYPES.SpreadElement &&
247
+ (index === bounds.length ||
248
+ (arg.type === AST_NODE_TYPES.Literal && arg.value === bounds[index])),
249
+ );
250
+ };
@@ -0,0 +1,307 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ },
13
+ },
14
+ });
15
+
16
+ // The folded-in comparison rules are type-aware, so they need a TypeScript
17
+ // program; the guard-normalization half does not.
18
+ const typedTester = new RuleTester({
19
+ languageOptions: {
20
+ parser,
21
+ parserOptions: {
22
+ ecmaVersion: 2020,
23
+ sourceType: 'module',
24
+ projectService: {
25
+ allowDefaultProject: ['*.ts*'],
26
+ },
27
+ tsconfigRootDir: `${import.meta.dirname}/../..`,
28
+ },
29
+ },
30
+ });
31
+
32
+ describe('prefer-canonical-length-guard (folded-in length comparisons)', () => {
33
+ typedTester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
34
+ valid: [
35
+ {
36
+ name: 'ignores non-array length comparisons',
37
+ code: dedent`
38
+ const str = 'hello';
39
+ const ok = str.length > 0;
40
+ `,
41
+ },
42
+ ],
43
+ invalid: [
44
+ {
45
+ name: 'xs.length > 0 becomes Arr.isNonEmpty(xs)',
46
+ code: dedent`
47
+ const xs = [1, 2, 3];
48
+ const ok = xs.length > 0;
49
+ `,
50
+ output: dedent`
51
+ import { Arr } from 'ts-data-forge';
52
+ const xs = [1, 2, 3];
53
+ const ok = Arr.isNonEmpty(xs);
54
+ `,
55
+ errors: [{ messageId: 'useIsNonEmpty' }],
56
+ },
57
+ {
58
+ name: 'xs.length >= 2 becomes Arr.isMinLengthArray(2, xs)',
59
+ code: dedent`
60
+ const xs = [1, 2, 3];
61
+ const ok = xs.length >= 2;
62
+ `,
63
+ output: dedent`
64
+ import { Arr } from 'ts-data-forge';
65
+ const xs = [1, 2, 3];
66
+ const ok = Arr.isMinLengthArray(2, xs);
67
+ `,
68
+ errors: [{ messageId: 'useIsMinLengthArray' }],
69
+ },
70
+ {
71
+ name: 'xs.length === 3 becomes Arr.isFixedLengthArray(3, xs)',
72
+ code: dedent`
73
+ const xs = [1, 2, 3];
74
+ const ok = xs.length === 3;
75
+ `,
76
+ output: dedent`
77
+ import { Arr } from 'ts-data-forge';
78
+ const xs = [1, 2, 3];
79
+ const ok = Arr.isFixedLengthArray(3, xs);
80
+ `,
81
+ errors: [{ messageId: 'useIsFixedLengthArray' }],
82
+ },
83
+ ],
84
+ });
85
+ });
86
+
87
+ describe('prefer-canonical-length-guard', () => {
88
+ tester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
89
+ valid: [
90
+ {
91
+ name: 'ignores non-degenerate bounds',
92
+ code: dedent`
93
+ import { Arr } from 'ts-data-forge';
94
+
95
+ declare const xs: readonly number[];
96
+ const ok = Arr.isFixedLengthTuple(2, xs);
97
+ `,
98
+ },
99
+ {
100
+ name: 'ignores isMinLengthArray with a bound other than 1',
101
+ code: dedent`
102
+ import { Arr } from 'ts-data-forge';
103
+
104
+ declare const xs: readonly number[];
105
+ const ok = Arr.isMinLengthArray(2, xs);
106
+ `,
107
+ },
108
+ {
109
+ name: 'ignores isBoundedLengthTuple unless both bounds are 0',
110
+ code: dedent`
111
+ import { Arr } from 'ts-data-forge';
112
+
113
+ declare const xs: readonly number[];
114
+ const ok = Arr.isBoundedLengthTuple(0, 1, xs);
115
+ `,
116
+ },
117
+ {
118
+ name: 'ignores a non-literal bound',
119
+ code: dedent`
120
+ import { Arr } from 'ts-data-forge';
121
+
122
+ declare const xs: readonly number[];
123
+ const n = 0;
124
+ const ok = Arr.isFixedLengthTuple(n, xs);
125
+ `,
126
+ },
127
+ {
128
+ name: 'ignores an Arr that is not from ts-data-forge',
129
+ code: dedent`
130
+ import { Arr } from './my-helpers.mjs';
131
+
132
+ declare const xs: readonly number[];
133
+ const ok = Arr.isFixedLengthTuple(0, xs);
134
+ `,
135
+ },
136
+ {
137
+ name: 'ignores already-canonical guards',
138
+ code: dedent`
139
+ import { Arr } from 'ts-data-forge';
140
+
141
+ declare const xs: readonly number[];
142
+ const ok = Arr.isEmpty(xs);
143
+ `,
144
+ },
145
+ {
146
+ name: 'ignores spread arguments',
147
+ code: dedent`
148
+ import { Arr } from 'ts-data-forge';
149
+
150
+ declare const args: readonly [readonly number[], 0];
151
+ const ok = Arr.isFixedLengthTuple(...args);
152
+ `,
153
+ },
154
+ ],
155
+ invalid: [
156
+ {
157
+ name: 'isFixedLengthArray(0, xs) becomes isEmpty(xs)',
158
+ code: dedent`
159
+ import { Arr } from 'ts-data-forge';
160
+
161
+ declare const xs: readonly number[];
162
+ const ok = Arr.isFixedLengthArray(0, xs);
163
+ `,
164
+ output: dedent`
165
+ import { Arr } from 'ts-data-forge';
166
+
167
+ declare const xs: readonly number[];
168
+ const ok = Arr.isEmpty(xs);
169
+ `,
170
+ errors: [{ messageId: 'useCanonicalGuard' }],
171
+ },
172
+ {
173
+ name: 'isMinLengthTuple(1, xs) becomes isNonEmptyTuple(xs)',
174
+ code: dedent`
175
+ import { Arr } from 'ts-data-forge';
176
+
177
+ declare const xs: readonly number[];
178
+ const ok = Arr.isMinLengthTuple(1, xs);
179
+ `,
180
+ output: dedent`
181
+ import { Arr } from 'ts-data-forge';
182
+
183
+ declare const xs: readonly number[];
184
+ const ok = Arr.isNonEmptyTuple(xs);
185
+ `,
186
+ errors: [{ messageId: 'useCanonicalGuard' }],
187
+ },
188
+ {
189
+ name: 'isFixedLengthTuple(0, xs) becomes isEmptyTuple(xs)',
190
+ code: dedent`
191
+ import { Arr } from 'ts-data-forge';
192
+
193
+ declare const xs: readonly number[];
194
+ const ok = Arr.isFixedLengthTuple(0, xs);
195
+ `,
196
+ output: dedent`
197
+ import { Arr } from 'ts-data-forge';
198
+
199
+ declare const xs: readonly number[];
200
+ const ok = Arr.isEmptyTuple(xs);
201
+ `,
202
+ errors: [{ messageId: 'useCanonicalGuard' }],
203
+ },
204
+ {
205
+ name: 'isMaxLengthTuple(0, xs) becomes isEmptyTuple(xs)',
206
+ code: dedent`
207
+ import { Arr } from 'ts-data-forge';
208
+
209
+ declare const xs: readonly number[];
210
+ const ok = Arr.isMaxLengthTuple(0, xs);
211
+ `,
212
+ output: dedent`
213
+ import { Arr } from 'ts-data-forge';
214
+
215
+ declare const xs: readonly number[];
216
+ const ok = Arr.isEmptyTuple(xs);
217
+ `,
218
+ errors: [{ messageId: 'useCanonicalGuard' }],
219
+ },
220
+ {
221
+ name: 'isBoundedLengthTuple(0, 0, xs) becomes isEmptyTuple(xs)',
222
+ code: dedent`
223
+ import { Arr } from 'ts-data-forge';
224
+
225
+ declare const xs: readonly number[];
226
+ const ok = Arr.isBoundedLengthTuple(0, 0, xs);
227
+ `,
228
+ output: dedent`
229
+ import { Arr } from 'ts-data-forge';
230
+
231
+ declare const xs: readonly number[];
232
+ const ok = Arr.isEmptyTuple(xs);
233
+ `,
234
+ errors: [{ messageId: 'useCanonicalGuard' }],
235
+ },
236
+ {
237
+ name: 'isMinLengthArray(1, xs) becomes isNonEmpty(xs)',
238
+ code: dedent`
239
+ import { Arr } from 'ts-data-forge';
240
+
241
+ declare const xs: readonly number[];
242
+ const ok = Arr.isMinLengthArray(1, xs);
243
+ `,
244
+ output: dedent`
245
+ import { Arr } from 'ts-data-forge';
246
+
247
+ declare const xs: readonly number[];
248
+ const ok = Arr.isNonEmpty(xs);
249
+ `,
250
+ errors: [{ messageId: 'useCanonicalGuard' }],
251
+ },
252
+ {
253
+ name: 'keeps a complex array expression intact',
254
+ code: dedent`
255
+ import { Arr } from 'ts-data-forge';
256
+
257
+ declare const obj: Readonly<{ items: readonly number[] }>;
258
+ const ok = Arr.isFixedLengthTuple(0, obj.items.filter((x) => x > 0));
259
+ `,
260
+ output: dedent`
261
+ import { Arr } from 'ts-data-forge';
262
+
263
+ declare const obj: Readonly<{ items: readonly number[] }>;
264
+ const ok = Arr.isEmptyTuple(obj.items.filter((x) => x > 0));
265
+ `,
266
+ errors: [{ messageId: 'useCanonicalGuard' }],
267
+ },
268
+ {
269
+ name: 'resolves an aliased Arr import',
270
+ code: dedent`
271
+ import { Arr as A } from 'ts-data-forge';
272
+
273
+ declare const xs: readonly number[];
274
+ const ok = A.isFixedLengthTuple(0, xs);
275
+ `,
276
+ output: dedent`
277
+ import { Arr as A } from 'ts-data-forge';
278
+
279
+ declare const xs: readonly number[];
280
+ const ok = A.isEmptyTuple(xs);
281
+ `,
282
+ errors: [{ messageId: 'useCanonicalGuard' }],
283
+ },
284
+ {
285
+ name: 'rewrites several guards in one file',
286
+ code: dedent`
287
+ import { Arr } from 'ts-data-forge';
288
+
289
+ declare const xs: readonly number[];
290
+ const a = Arr.isFixedLengthTuple(0, xs);
291
+ const b = Arr.isMinLengthArray(1, xs);
292
+ `,
293
+ output: dedent`
294
+ import { Arr } from 'ts-data-forge';
295
+
296
+ declare const xs: readonly number[];
297
+ const a = Arr.isEmptyTuple(xs);
298
+ const b = Arr.isNonEmpty(xs);
299
+ `,
300
+ errors: [
301
+ { messageId: 'useCanonicalGuard' },
302
+ { messageId: 'useCanonicalGuard' },
303
+ ],
304
+ },
305
+ ],
306
+ });
307
+ });
@@ -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
  }
@@ -1,14 +1,11 @@
1
1
  import { type ESLintPlugin } from '../types.mjs';
2
2
  import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
3
3
  import { preferArrIsArray } from './prefer-arr-is-array.mjs';
4
- import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
5
- import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
6
- import { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';
7
- import { preferArrIsMinLengthArray } from './prefer-arr-is-min-length-array.mjs';
8
- import { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';
9
4
  import { preferArrSum } from './prefer-arr-sum.mjs';
10
5
  import { preferAsInt } from './prefer-as-int.mjs';
11
6
  import { preferCanonicalArraySlicing } from './prefer-canonical-array-slicing.mjs';
7
+ import { preferCanonicalLengthCast } from './prefer-canonical-length-cast.mjs';
8
+ import { preferCanonicalLengthGuard } from './prefer-canonical-length-guard.mjs';
12
9
  import { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';
13
10
  import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
14
11
  import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
@@ -18,12 +15,9 @@ import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
18
15
 
19
16
  export const tsDataForgeRules = {
20
17
  'prefer-canonical-array-slicing': preferCanonicalArraySlicing,
21
- 'prefer-arr-is-min-length-array': preferArrIsMinLengthArray,
22
- 'prefer-arr-is-max-length-array': preferArrIsMaxLengthArray,
23
- 'prefer-arr-is-bounded-length-array': preferArrIsBoundedLengthArray,
24
- 'prefer-arr-is-fixed-length-array': preferArrIsFixedLengthArray,
18
+ 'prefer-canonical-length-guard': preferCanonicalLengthGuard,
19
+ 'prefer-canonical-length-cast': preferCanonicalLengthCast,
25
20
  'prefer-arr-is-array': preferArrIsArray,
26
- 'prefer-arr-is-non-empty': preferArrIsNonEmpty,
27
21
  'prefer-arr-sum': preferArrSum,
28
22
  'prefer-as-int': preferAsInt,
29
23
  'prefer-is-non-null-object': preferIsNonNullObject,
package/src/types.mts CHANGED
@@ -2,3 +2,5 @@ import { type TSESLint } from '@typescript-eslint/utils';
2
2
  import { type DeepReadonly } from 'ts-type-forge';
3
3
 
4
4
  export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
5
+
6
+ export type ESLintFlatConfig = DeepReadonly<TSESLint.FlatConfig.Config>;