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.
- package/README.md +49 -19
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/plugin.d.mts +46 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +35 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +6 -51
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +2 -2
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts +6 -0
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -0
- package/dist/rules/prefer-canonical-length-cast.mjs +155 -0
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -0
- package/dist/rules/prefer-canonical-length-guard.d.mts +6 -0
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -0
- package/dist/rules/prefer-canonical-length-guard.mjs +129 -0
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -0
- package/dist/rules/prefer-range-for-loop.mjs +1 -1
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +2 -5
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +4 -10
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/types.d.mts +1 -0
- package/dist/types.d.mts.map +1 -1
- package/package.json +7 -3
- package/src/index.mts +1 -1
- package/src/plugin.mts +47 -3
- package/src/plugin.test.mts +24 -0
- package/src/rule-types.mts +6 -54
- package/src/rules/import-utils.mts +0 -1
- package/src/rules/prefer-arr-is-bounded-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +5 -5
- package/src/rules/prefer-arr-is-fixed-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +11 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -8
- package/src/rules/prefer-arr-is-min-length-array.mts +1 -1
- package/src/rules/prefer-arr-is-min-length-array.test.mts +9 -9
- package/src/rules/prefer-arr-sum.mts +2 -2
- package/src/rules/prefer-canonical-array-slicing.mts +0 -2
- package/src/rules/prefer-canonical-length-cast.mts +290 -0
- package/src/rules/prefer-canonical-length-cast.test.mts +305 -0
- package/src/rules/prefer-canonical-length-guard.mts +250 -0
- package/src/rules/prefer-canonical-length-guard.test.mts +307 -0
- package/src/rules/prefer-range-for-loop.mts +1 -1
- package/src/rules/rules.mts +4 -10
- package/src/types.mts +2 -0
|
@@ -123,7 +123,7 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
|
|
|
123
123
|
length: lengthText,
|
|
124
124
|
},
|
|
125
125
|
fix: (fixer) => {
|
|
126
|
-
const replacement = `Arr.isMinLengthArray(${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
180
|
-
const ok2 = Arr.isMinLengthArray(
|
|
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(
|
|
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
|
|
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
|
|
81
|
+
!Arr.isFixedLengthTuple(2, reducer.params)
|
|
82
82
|
) {
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
@@ -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
|
+
});
|