eslint-plugin-ts-data-forge 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -14
- package/dist/plugin.d.mts +3 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +13 -1
- package/dist/rules/no-unnecessary-type-guard.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.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +12 -8
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +30 -16
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- 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 -1
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +6 -6
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/import-utils.mts +0 -1
- package/src/rules/no-unnecessary-type-guard.mts +65 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +94 -0
- 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 +40 -21
- package/src/rules/prefer-canonical-length-guard.test.mts +32 -32
- package/src/rules/prefer-is-record-and-has-key.mts +84 -20
- package/src/rules/prefer-is-record-and-has-key.test.mts +74 -0
- package/src/rules/prefer-range-for-loop.mts +1 -1
- package/src/rules/rules.mts +2 -0
|
@@ -85,7 +85,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
85
85
|
output: dedent`
|
|
86
86
|
import { Arr } from 'ts-data-forge';
|
|
87
87
|
const xs: readonly number[] = [1, 2, 3];
|
|
88
|
-
const ok = Arr.isBoundedLengthArray(
|
|
88
|
+
const ok = Arr.isBoundedLengthArray(1, 5, xs);
|
|
89
89
|
`,
|
|
90
90
|
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
91
91
|
},
|
|
@@ -98,7 +98,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
98
98
|
output: dedent`
|
|
99
99
|
import { Arr } from 'ts-data-forge';
|
|
100
100
|
const xs: readonly number[] = [1, 2, 3];
|
|
101
|
-
const ok = Arr.isBoundedLengthArray(
|
|
101
|
+
const ok = Arr.isBoundedLengthArray(1, 5, xs);
|
|
102
102
|
`,
|
|
103
103
|
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
104
104
|
},
|
|
@@ -111,7 +111,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
111
111
|
output: dedent`
|
|
112
112
|
import { Arr } from 'ts-data-forge';
|
|
113
113
|
const xs: readonly number[] = [1, 2, 3];
|
|
114
|
-
const ok = Arr.isBoundedLengthArray(
|
|
114
|
+
const ok = Arr.isBoundedLengthArray(1, 5, xs);
|
|
115
115
|
`,
|
|
116
116
|
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
117
117
|
},
|
|
@@ -128,7 +128,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
128
128
|
const xs = [1, 2, 3];
|
|
129
129
|
const lo = 1;
|
|
130
130
|
const hi = 5;
|
|
131
|
-
const ok = Arr.isBoundedLengthArray(
|
|
131
|
+
const ok = Arr.isBoundedLengthArray(lo, hi, xs);
|
|
132
132
|
`,
|
|
133
133
|
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
134
134
|
},
|
|
@@ -144,7 +144,7 @@ describe('prefer-arr-is-bounded-length-array', () => {
|
|
|
144
144
|
import { Arr } from 'ts-data-forge';
|
|
145
145
|
|
|
146
146
|
const xs = [1, 2, 3];
|
|
147
|
-
const ok = Arr.isBoundedLengthArray(
|
|
147
|
+
const ok = Arr.isBoundedLengthArray(1, 5, xs);
|
|
148
148
|
`,
|
|
149
149
|
errors: [{ messageId: 'useIsBoundedLengthArray' }],
|
|
150
150
|
},
|
|
@@ -122,7 +122,7 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
|
|
|
122
122
|
length: lengthText,
|
|
123
123
|
},
|
|
124
124
|
fix: (fixer) => {
|
|
125
|
-
const baseCall = `Arr.isFixedLengthArray(${
|
|
125
|
+
const baseCall = `Arr.isFixedLengthArray(${lengthText}, ${arrayText})`;
|
|
126
126
|
|
|
127
127
|
const replacement = isNegated ? `!${baseCall}` : baseCall;
|
|
128
128
|
|
|
@@ -76,7 +76,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
76
76
|
output: dedent`
|
|
77
77
|
import { Arr } from 'ts-data-forge';
|
|
78
78
|
const xs: readonly number[] = [1, 2, 3];
|
|
79
|
-
const ok = Arr.isFixedLengthArray(
|
|
79
|
+
const ok = Arr.isFixedLengthArray(3, xs);
|
|
80
80
|
`,
|
|
81
81
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
82
82
|
},
|
|
@@ -89,7 +89,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
89
89
|
output: dedent`
|
|
90
90
|
import { Arr } from 'ts-data-forge';
|
|
91
91
|
const xs: readonly number[] = [1, 2, 3];
|
|
92
|
-
const ok = Arr.isFixedLengthArray(
|
|
92
|
+
const ok = Arr.isFixedLengthArray(3, xs);
|
|
93
93
|
`,
|
|
94
94
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
95
95
|
},
|
|
@@ -102,7 +102,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
102
102
|
output: dedent`
|
|
103
103
|
import { Arr } from 'ts-data-forge';
|
|
104
104
|
const xs = [1, 2, 3];
|
|
105
|
-
const ok = Arr.isFixedLengthArray(
|
|
105
|
+
const ok = Arr.isFixedLengthArray(3, xs);
|
|
106
106
|
`,
|
|
107
107
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
108
108
|
},
|
|
@@ -115,7 +115,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
115
115
|
output: dedent`
|
|
116
116
|
import { Arr } from 'ts-data-forge';
|
|
117
117
|
const xs = [1, 2, 3] as const;
|
|
118
|
-
const ok = Arr.isFixedLengthArray(
|
|
118
|
+
const ok = Arr.isFixedLengthArray(3, xs);
|
|
119
119
|
`,
|
|
120
120
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
121
121
|
},
|
|
@@ -130,7 +130,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
130
130
|
import { Arr } from 'ts-data-forge';
|
|
131
131
|
const xs = [1, 2, 3];
|
|
132
132
|
const n = 3;
|
|
133
|
-
const ok = Arr.isFixedLengthArray(
|
|
133
|
+
const ok = Arr.isFixedLengthArray(n, xs);
|
|
134
134
|
`,
|
|
135
135
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
136
136
|
},
|
|
@@ -146,7 +146,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
146
146
|
import { Arr } from 'ts-data-forge';
|
|
147
147
|
|
|
148
148
|
const xs = [1, 2, 3];
|
|
149
|
-
const ok = Arr.isFixedLengthArray(
|
|
149
|
+
const ok = Arr.isFixedLengthArray(3, xs);
|
|
150
150
|
`,
|
|
151
151
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
152
152
|
},
|
|
@@ -162,8 +162,8 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
162
162
|
import { Arr } from 'ts-data-forge';
|
|
163
163
|
const xs = [1, 2, 3];
|
|
164
164
|
const ys = [4, 5];
|
|
165
|
-
const ok1 = Arr.isFixedLengthArray(
|
|
166
|
-
const ok2 = Arr.isFixedLengthArray(
|
|
165
|
+
const ok1 = Arr.isFixedLengthArray(3, xs);
|
|
166
|
+
const ok2 = Arr.isFixedLengthArray(2, ys);
|
|
167
167
|
`,
|
|
168
168
|
errors: [
|
|
169
169
|
{ messageId: 'useIsFixedLengthArray' },
|
|
@@ -179,7 +179,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
179
179
|
output: dedent`
|
|
180
180
|
import { Arr } from 'ts-data-forge';
|
|
181
181
|
const xs: readonly number[] = [1, 2, 3];
|
|
182
|
-
const ok = !Arr.isFixedLengthArray(
|
|
182
|
+
const ok = !Arr.isFixedLengthArray(3, xs);
|
|
183
183
|
`,
|
|
184
184
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
185
185
|
},
|
|
@@ -192,7 +192,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
192
192
|
output: dedent`
|
|
193
193
|
import { Arr } from 'ts-data-forge';
|
|
194
194
|
const xs: readonly number[] = [1, 2, 3];
|
|
195
|
-
const ok = !Arr.isFixedLengthArray(
|
|
195
|
+
const ok = !Arr.isFixedLengthArray(3, xs);
|
|
196
196
|
`,
|
|
197
197
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
198
198
|
},
|
|
@@ -205,7 +205,7 @@ describe('prefer-arr-is-fixed-length-array', () => {
|
|
|
205
205
|
output: dedent`
|
|
206
206
|
import { Arr } from 'ts-data-forge';
|
|
207
207
|
const xs = [1, 2, 3];
|
|
208
|
-
const ok = !Arr.isFixedLengthArray(
|
|
208
|
+
const ok = !Arr.isFixedLengthArray(5, xs);
|
|
209
209
|
`,
|
|
210
210
|
errors: [{ messageId: 'useIsFixedLengthArray' }],
|
|
211
211
|
},
|
|
@@ -123,7 +123,7 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
|
|
|
123
123
|
length: lengthText,
|
|
124
124
|
},
|
|
125
125
|
fix: (fixer) => {
|
|
126
|
-
const replacement = `Arr.isMaxLengthArray(${
|
|
126
|
+
const replacement = `Arr.isMaxLengthArray(${lengthText}, ${arrayText})`;
|
|
127
127
|
|
|
128
128
|
const importFixes =
|
|
129
129
|
index === 0 && !hasArrImport
|
|
@@ -89,7 +89,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
89
89
|
output: dedent`
|
|
90
90
|
import { Arr } from 'ts-data-forge';
|
|
91
91
|
const xs: readonly number[] = [1, 2, 3];
|
|
92
|
-
const ok = Arr.isMaxLengthArray(
|
|
92
|
+
const ok = Arr.isMaxLengthArray(3, xs);
|
|
93
93
|
`,
|
|
94
94
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
95
95
|
},
|
|
@@ -102,7 +102,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
102
102
|
output: dedent`
|
|
103
103
|
import { Arr } from 'ts-data-forge';
|
|
104
104
|
const xs: readonly number[] = [1, 2, 3];
|
|
105
|
-
const ok = Arr.isMaxLengthArray(
|
|
105
|
+
const ok = Arr.isMaxLengthArray(3, xs);
|
|
106
106
|
`,
|
|
107
107
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
108
108
|
},
|
|
@@ -115,7 +115,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
115
115
|
output: dedent`
|
|
116
116
|
import { Arr } from 'ts-data-forge';
|
|
117
117
|
const xs = [1, 2, 3];
|
|
118
|
-
const ok = Arr.isMaxLengthArray(
|
|
118
|
+
const ok = Arr.isMaxLengthArray(1, xs);
|
|
119
119
|
`,
|
|
120
120
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
121
121
|
},
|
|
@@ -128,7 +128,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
128
128
|
output: dedent`
|
|
129
129
|
import { Arr } from 'ts-data-forge';
|
|
130
130
|
const xs = [1, 2, 3] as const;
|
|
131
|
-
const ok = Arr.isMaxLengthArray(
|
|
131
|
+
const ok = Arr.isMaxLengthArray(2, xs);
|
|
132
132
|
`,
|
|
133
133
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
134
134
|
},
|
|
@@ -143,7 +143,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
143
143
|
import { Arr } from 'ts-data-forge';
|
|
144
144
|
const xs = [1, 2, 3];
|
|
145
145
|
const n = 2;
|
|
146
|
-
const ok = Arr.isMaxLengthArray(
|
|
146
|
+
const ok = Arr.isMaxLengthArray(n, xs);
|
|
147
147
|
`,
|
|
148
148
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
149
149
|
},
|
|
@@ -159,7 +159,7 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
159
159
|
import { Arr } from 'ts-data-forge';
|
|
160
160
|
|
|
161
161
|
const xs = [1, 2, 3];
|
|
162
|
-
const ok = Arr.isMaxLengthArray(
|
|
162
|
+
const ok = Arr.isMaxLengthArray(1, xs);
|
|
163
163
|
`,
|
|
164
164
|
errors: [{ messageId: 'useIsMaxLengthArray' }],
|
|
165
165
|
},
|
|
@@ -175,8 +175,8 @@ describe('prefer-arr-is-max-length-array', () => {
|
|
|
175
175
|
import { Arr } from 'ts-data-forge';
|
|
176
176
|
const xs = [1, 2, 3];
|
|
177
177
|
const ys = [4, 5];
|
|
178
|
-
const ok1 = Arr.isMaxLengthArray(
|
|
179
|
-
const ok2 = Arr.isMaxLengthArray(
|
|
178
|
+
const ok1 = Arr.isMaxLengthArray(2, xs);
|
|
179
|
+
const ok2 = Arr.isMaxLengthArray(1, ys);
|
|
180
180
|
`,
|
|
181
181
|
errors: [
|
|
182
182
|
{ messageId: 'useIsMaxLengthArray' },
|
|
@@ -123,7 +123,7 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
|
|
|
123
123
|
length: lengthText,
|
|
124
124
|
},
|
|
125
125
|
fix: (fixer) => {
|
|
126
|
-
const replacement = `Arr.isMinLengthArray(${
|
|
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;
|