eslint-plugin-ts-data-forge 0.1.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/LICENSE +201 -0
- package/README.md +57 -0
- package/package.json +74 -0
- package/src/index.mts +7 -0
- package/src/plugin.mts +9 -0
- package/src/rule-types.mts +326 -0
- package/src/rules/ast-utils.mts +169 -0
- package/src/rules/branded-number-types.mts +47 -0
- package/src/rules/constants.mts +1 -0
- package/src/rules/import-utils.mts +152 -0
- package/src/rules/index.mts +1 -0
- package/src/rules/no-unnecessary-type-guard.mts +515 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +368 -0
- package/src/rules/prefer-arr-is-array.mts +98 -0
- package/src/rules/prefer-arr-is-array.test.mts +62 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +143 -0
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +154 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +149 -0
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +214 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +188 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +202 -0
- package/src/rules/prefer-arr-is-non-empty.mts +131 -0
- package/src/rules/prefer-arr-is-non-empty.test.mts +90 -0
- package/src/rules/prefer-arr-sum.mts +269 -0
- package/src/rules/prefer-arr-sum.test.mts +171 -0
- package/src/rules/prefer-as-int.mts +131 -0
- package/src/rules/prefer-as-int.test.mts +267 -0
- package/src/rules/prefer-canonical-array-slicing.mts +800 -0
- package/src/rules/prefer-canonical-array-slicing.test.mts +588 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +141 -0
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +171 -0
- package/src/rules/prefer-is-non-null-object.mts +145 -0
- package/src/rules/prefer-is-non-null-object.test.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.test.mts +217 -0
- package/src/rules/prefer-num-safe-parse-float.mts +166 -0
- package/src/rules/prefer-num-safe-parse-float.test.mts +163 -0
- package/src/rules/prefer-num-safe-parse-int.mts +166 -0
- package/src/rules/prefer-num-safe-parse-int.test.mts +137 -0
- package/src/rules/prefer-range-for-loop.mts +179 -0
- package/src/rules/prefer-range-for-loop.test.mts +156 -0
- package/src/rules/rules.mts +36 -0
- package/src/types.mts +4 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
projectService: {
|
|
13
|
+
allowDefaultProject: ['*.ts*'],
|
|
14
|
+
},
|
|
15
|
+
tsconfigRootDir: `${import.meta.dirname}/../..`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('no-unnecessary-type-guard', () => {
|
|
21
|
+
tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'isNotUndefined on a type that includes undefined',
|
|
25
|
+
code: dedent`
|
|
26
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
27
|
+
declare const x: string | undefined;
|
|
28
|
+
const y = isNotUndefined(x);
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'isNonNullish on a type that includes both null and undefined',
|
|
33
|
+
code: dedent`
|
|
34
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
35
|
+
declare const x: string | null | undefined;
|
|
36
|
+
const y = isNonNullish(x);
|
|
37
|
+
`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'isBoolean that actually narrows',
|
|
41
|
+
code: dedent`
|
|
42
|
+
import { isBoolean } from 'ts-data-forge';
|
|
43
|
+
declare const x: boolean | string;
|
|
44
|
+
const y = isBoolean(x);
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'isNotBoolean that actually narrows',
|
|
49
|
+
code: dedent`
|
|
50
|
+
import { isNotBoolean } from 'ts-data-forge';
|
|
51
|
+
declare const x: boolean | string;
|
|
52
|
+
const y = isNotBoolean(x);
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'isNonEmptyString on a plain string (real string/empty work)',
|
|
57
|
+
code: dedent`
|
|
58
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
59
|
+
declare const x: string | undefined;
|
|
60
|
+
const y = isNonEmptyString(x);
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'isNonEmptyString on a union that may be empty',
|
|
65
|
+
code: dedent`
|
|
66
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
67
|
+
declare const x: 'a' | '';
|
|
68
|
+
const y = isNonEmptyString(x);
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'generic type parameter is treated conservatively',
|
|
73
|
+
code: dedent`
|
|
74
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
75
|
+
const f = <T,>(x: T) => isNotUndefined(x);
|
|
76
|
+
`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'any is treated conservatively',
|
|
80
|
+
code: dedent`
|
|
81
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
82
|
+
declare const x: any;
|
|
83
|
+
const y = isNonNullish(x);
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'identifier not imported from ts-data-forge is ignored',
|
|
88
|
+
code: dedent`
|
|
89
|
+
const isNotUndefined = (u: unknown): boolean => u !== undefined;
|
|
90
|
+
declare const x: string;
|
|
91
|
+
const y = isNotUndefined(x);
|
|
92
|
+
`,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'guard passed by reference (not called) is ignored',
|
|
96
|
+
code: dedent`
|
|
97
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
98
|
+
declare const xs: readonly (string | undefined)[];
|
|
99
|
+
const ys = xs.filter(isNotUndefined);
|
|
100
|
+
`,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'ignore option disables a specific guard',
|
|
104
|
+
code: dedent`
|
|
105
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
106
|
+
declare const x: string;
|
|
107
|
+
const y = isNotUndefined(x);
|
|
108
|
+
`,
|
|
109
|
+
options: [{ ignore: ['isNotUndefined'] }],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
invalid: [
|
|
113
|
+
{
|
|
114
|
+
name: 'isNotUndefined on a type that cannot be undefined (always true)',
|
|
115
|
+
code: dedent`
|
|
116
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
117
|
+
declare const x: string;
|
|
118
|
+
const y = isNotUndefined(x);
|
|
119
|
+
`,
|
|
120
|
+
output: dedent`
|
|
121
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
122
|
+
declare const x: string;
|
|
123
|
+
const y = true;
|
|
124
|
+
`,
|
|
125
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'isBoolean on a value that is already boolean (always true)',
|
|
129
|
+
code: dedent`
|
|
130
|
+
import { isBoolean } from 'ts-data-forge';
|
|
131
|
+
declare const x: boolean;
|
|
132
|
+
const y = isBoolean(x);
|
|
133
|
+
`,
|
|
134
|
+
output: dedent`
|
|
135
|
+
import { isBoolean } from 'ts-data-forge';
|
|
136
|
+
declare const x: boolean;
|
|
137
|
+
const y = true;
|
|
138
|
+
`,
|
|
139
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'isNotBoolean on a value that can never be boolean (always true)',
|
|
143
|
+
code: dedent`
|
|
144
|
+
import { isNotBoolean } from 'ts-data-forge';
|
|
145
|
+
declare const x: string | number;
|
|
146
|
+
const y = isNotBoolean(x);
|
|
147
|
+
`,
|
|
148
|
+
output: dedent`
|
|
149
|
+
import { isNotBoolean } from 'ts-data-forge';
|
|
150
|
+
declare const x: string | number;
|
|
151
|
+
const y = true;
|
|
152
|
+
`,
|
|
153
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'isNonNullish on a non-nullable value (always true)',
|
|
157
|
+
code: dedent`
|
|
158
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
159
|
+
declare const x: string;
|
|
160
|
+
const y = isNonNullish(x);
|
|
161
|
+
`,
|
|
162
|
+
output: dedent`
|
|
163
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
164
|
+
declare const x: string;
|
|
165
|
+
const y = true;
|
|
166
|
+
`,
|
|
167
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'isNullish on a value that is already null | undefined (always true)',
|
|
171
|
+
code: dedent`
|
|
172
|
+
import { isNullish } from 'ts-data-forge';
|
|
173
|
+
declare const x: null | undefined;
|
|
174
|
+
const y = isNullish(x);
|
|
175
|
+
`,
|
|
176
|
+
output: dedent`
|
|
177
|
+
import { isNullish } from 'ts-data-forge';
|
|
178
|
+
declare const x: null | undefined;
|
|
179
|
+
const y = true;
|
|
180
|
+
`,
|
|
181
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'isNonEmptyString on a NonEmptyString value (always true)',
|
|
185
|
+
code: dedent`
|
|
186
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
187
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
188
|
+
declare const x: NonEmptyString;
|
|
189
|
+
const y = isNonEmptyString(x);
|
|
190
|
+
`,
|
|
191
|
+
output: dedent`
|
|
192
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
193
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
194
|
+
declare const x: NonEmptyString;
|
|
195
|
+
const y = true;
|
|
196
|
+
`,
|
|
197
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'isNonEmptyString on a non-empty string literal union (always true)',
|
|
201
|
+
code: dedent`
|
|
202
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
203
|
+
declare const x: 'a' | 'b';
|
|
204
|
+
const y = isNonEmptyString(x);
|
|
205
|
+
`,
|
|
206
|
+
output: dedent`
|
|
207
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
208
|
+
declare const x: 'a' | 'b';
|
|
209
|
+
const y = true;
|
|
210
|
+
`,
|
|
211
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'isBoolean on a value that can never be boolean (always false)',
|
|
215
|
+
code: dedent`
|
|
216
|
+
import { isBoolean } from 'ts-data-forge';
|
|
217
|
+
declare const x: string;
|
|
218
|
+
const y = isBoolean(x);
|
|
219
|
+
`,
|
|
220
|
+
output: dedent`
|
|
221
|
+
import { isBoolean } from 'ts-data-forge';
|
|
222
|
+
declare const x: string;
|
|
223
|
+
const y = false;
|
|
224
|
+
`,
|
|
225
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'isNotUndefined on an always-undefined value (always false)',
|
|
229
|
+
code: dedent`
|
|
230
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
231
|
+
declare const x: undefined;
|
|
232
|
+
const y = isNotUndefined(x);
|
|
233
|
+
`,
|
|
234
|
+
output: dedent`
|
|
235
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
236
|
+
declare const x: undefined;
|
|
237
|
+
const y = false;
|
|
238
|
+
`,
|
|
239
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'isNonNullish on an always-nullish value (always false)',
|
|
243
|
+
code: dedent`
|
|
244
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
245
|
+
declare const x: null | undefined;
|
|
246
|
+
const y = isNonNullish(x);
|
|
247
|
+
`,
|
|
248
|
+
output: dedent`
|
|
249
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
250
|
+
declare const x: null | undefined;
|
|
251
|
+
const y = false;
|
|
252
|
+
`,
|
|
253
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'isNonEmptyString on the empty string literal (always false)',
|
|
257
|
+
code: dedent`
|
|
258
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
259
|
+
declare const x: '';
|
|
260
|
+
const y = isNonEmptyString(x);
|
|
261
|
+
`,
|
|
262
|
+
output: dedent`
|
|
263
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
264
|
+
declare const x: '';
|
|
265
|
+
const y = false;
|
|
266
|
+
`,
|
|
267
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'no autofix when the argument has side effects',
|
|
271
|
+
code: dedent`
|
|
272
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
273
|
+
declare const getValue: () => string;
|
|
274
|
+
const y = isNotUndefined(getValue());
|
|
275
|
+
`,
|
|
276
|
+
output: null,
|
|
277
|
+
errors: [{ messageId: 'alwaysTrue' }],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'isNonNullish that only removes undefined -> isNotUndefined',
|
|
281
|
+
code: dedent`
|
|
282
|
+
import { isNonNullish, isNotUndefined } from 'ts-data-forge';
|
|
283
|
+
declare const x: string | undefined;
|
|
284
|
+
const y = isNonNullish(x);
|
|
285
|
+
`,
|
|
286
|
+
output: dedent`
|
|
287
|
+
import { isNonNullish, isNotUndefined } from 'ts-data-forge';
|
|
288
|
+
declare const x: string | undefined;
|
|
289
|
+
const y = isNotUndefined(x);
|
|
290
|
+
`,
|
|
291
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'replacement guard imported under an alias renames to the alias',
|
|
295
|
+
code: dedent`
|
|
296
|
+
import { isNonNullish, isNotUndefined as inu } from 'ts-data-forge';
|
|
297
|
+
declare const x: string | undefined;
|
|
298
|
+
const y = isNonNullish(x);
|
|
299
|
+
`,
|
|
300
|
+
output: dedent`
|
|
301
|
+
import { isNonNullish, isNotUndefined as inu } from 'ts-data-forge';
|
|
302
|
+
declare const x: string | undefined;
|
|
303
|
+
const y = inu(x);
|
|
304
|
+
`,
|
|
305
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'isNonNullish that only removes null -> isNotNull (import inserted)',
|
|
309
|
+
code: dedent`
|
|
310
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
311
|
+
declare const x: string | null;
|
|
312
|
+
const y = isNonNullish(x);
|
|
313
|
+
`,
|
|
314
|
+
output: dedent`
|
|
315
|
+
import { isNotNull } from 'ts-data-forge';
|
|
316
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
317
|
+
declare const x: string | null;
|
|
318
|
+
const y = isNotNull(x);
|
|
319
|
+
`,
|
|
320
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'isNullish that only matches undefined -> isUndefined',
|
|
324
|
+
code: dedent`
|
|
325
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
326
|
+
declare const x: string | undefined;
|
|
327
|
+
const y = isNullish(x);
|
|
328
|
+
`,
|
|
329
|
+
output: dedent`
|
|
330
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
331
|
+
declare const x: string | undefined;
|
|
332
|
+
const y = isUndefined(x);
|
|
333
|
+
`,
|
|
334
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'isNonEmptyString that only removes nullish -> isNonNullish',
|
|
338
|
+
code: dedent`
|
|
339
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
340
|
+
import { isNonEmptyString, isNonNullish } from 'ts-data-forge';
|
|
341
|
+
declare const x: NonEmptyString | null | undefined;
|
|
342
|
+
const y = isNonEmptyString(x);
|
|
343
|
+
`,
|
|
344
|
+
output: dedent`
|
|
345
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
346
|
+
import { isNonEmptyString, isNonNullish } from 'ts-data-forge';
|
|
347
|
+
declare const x: NonEmptyString | null | undefined;
|
|
348
|
+
const y = isNonNullish(x);
|
|
349
|
+
`,
|
|
350
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'namespace import member call is replaced without import change',
|
|
354
|
+
code: dedent`
|
|
355
|
+
import * as tf from 'ts-data-forge';
|
|
356
|
+
declare const x: string | undefined;
|
|
357
|
+
const y = tf.isNonNullish(x);
|
|
358
|
+
`,
|
|
359
|
+
output: dedent`
|
|
360
|
+
import * as tf from 'ts-data-forge';
|
|
361
|
+
declare const x: string | undefined;
|
|
362
|
+
const y = tf.isNotUndefined(x);
|
|
363
|
+
`,
|
|
364
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsDataForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useArrIsArray';
|
|
16
|
+
|
|
17
|
+
export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'suggestion',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.',
|
|
23
|
+
},
|
|
24
|
+
fixable: 'code',
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
useArrIsArray:
|
|
28
|
+
'Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create: (context) => {
|
|
33
|
+
const sourceCode = context.sourceCode;
|
|
34
|
+
|
|
35
|
+
const program = sourceCode.ast;
|
|
36
|
+
|
|
37
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
38
|
+
|
|
39
|
+
const mut_nodesToFix: TSESTree.CallExpression[] = [];
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
CallExpression: (node) => {
|
|
43
|
+
if (!isArrayIsArrayCall(node)) return;
|
|
44
|
+
|
|
45
|
+
mut_nodesToFix.push(node);
|
|
46
|
+
},
|
|
47
|
+
'Program:exit': () => {
|
|
48
|
+
// Check if Arr is already imported
|
|
49
|
+
const hasArrImport = getNamedImports(tsDataForgeImport).includes('Arr');
|
|
50
|
+
|
|
51
|
+
// Note: We add import only for the first node to avoid conflicts when
|
|
52
|
+
// multiple fixes try to insert at the same position. This means if the
|
|
53
|
+
// first node is disabled via eslint-disable comment, no import will be
|
|
54
|
+
// added.
|
|
55
|
+
for (const [index, node] of mut_nodesToFix.entries()) {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: 'useArrIsArray',
|
|
59
|
+
fix: (fixer) => {
|
|
60
|
+
const callee = node.callee;
|
|
61
|
+
|
|
62
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return [];
|
|
63
|
+
|
|
64
|
+
const replacement = `Arr.isArray${sourceCode.getText(node).slice(sourceCode.getText(callee).length)}`;
|
|
65
|
+
|
|
66
|
+
// Add import only for the first node and only if not already imported
|
|
67
|
+
const importFixes =
|
|
68
|
+
index === 0 && !hasArrImport
|
|
69
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
defaultOptions: [],
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
const isArrayIsArrayCall = (
|
|
83
|
+
node: DeepReadonly<TSESTree.CallExpression>,
|
|
84
|
+
): boolean => {
|
|
85
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
86
|
+
|
|
87
|
+
const { object, property } = node.callee;
|
|
88
|
+
|
|
89
|
+
if (object.type !== AST_NODE_TYPES.Identifier) return false;
|
|
90
|
+
|
|
91
|
+
if (object.name !== 'Array') return false;
|
|
92
|
+
|
|
93
|
+
if (property.type !== AST_NODE_TYPES.Identifier) return false;
|
|
94
|
+
|
|
95
|
+
if (property.name !== 'isArray') return false;
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { preferArrIsArray } from './prefer-arr-is-array.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'Already using Arr.isArray',
|
|
11
|
+
code: dedent`
|
|
12
|
+
import { Arr } from 'ts-data-forge';
|
|
13
|
+
const result = Arr.isArray(value);
|
|
14
|
+
`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Not Array.isArray call',
|
|
18
|
+
code: dedent`
|
|
19
|
+
const obj = { isArray: () => true };
|
|
20
|
+
const result = obj.isArray(value);
|
|
21
|
+
`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
invalid: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Replace Array.isArray with Arr.isArray',
|
|
27
|
+
code: dedent`
|
|
28
|
+
const result = Array.isArray(value);
|
|
29
|
+
`,
|
|
30
|
+
output: dedent`
|
|
31
|
+
import { Arr } from 'ts-data-forge';
|
|
32
|
+
const result = Arr.isArray(value);
|
|
33
|
+
`,
|
|
34
|
+
errors: [{ messageId: 'useArrIsArray' }],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Replace Array.isArray with Arr.isArray when Arr is already imported',
|
|
38
|
+
code: dedent`
|
|
39
|
+
import { Arr } from 'ts-data-forge';
|
|
40
|
+
const result = Array.isArray(value);
|
|
41
|
+
`,
|
|
42
|
+
output: dedent`
|
|
43
|
+
import { Arr } from 'ts-data-forge';
|
|
44
|
+
const result = Arr.isArray(value);
|
|
45
|
+
`,
|
|
46
|
+
errors: [{ messageId: 'useArrIsArray' }],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Replace multiple Array.isArray calls',
|
|
50
|
+
code: dedent`
|
|
51
|
+
const result1 = Array.isArray(value1);
|
|
52
|
+
const result2 = Array.isArray(value2);
|
|
53
|
+
`,
|
|
54
|
+
output: dedent`
|
|
55
|
+
import { Arr } from 'ts-data-forge';
|
|
56
|
+
const result1 = Arr.isArray(value1);
|
|
57
|
+
const result2 = Arr.isArray(value2);
|
|
58
|
+
`,
|
|
59
|
+
errors: [{ messageId: 'useArrIsArray' }, { messageId: 'useArrIsArray' }],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { parseLengthComparison } from './ast-utils.mjs';
|
|
3
|
+
import {
|
|
4
|
+
buildImportFixes,
|
|
5
|
+
getNamedImports,
|
|
6
|
+
getTsDataForgeImport,
|
|
7
|
+
} from './import-utils.mjs';
|
|
8
|
+
|
|
9
|
+
type Options = readonly [];
|
|
10
|
+
|
|
11
|
+
type MessageIds = 'useIsBoundedLengthArray';
|
|
12
|
+
|
|
13
|
+
export const preferArrIsBoundedLengthArray: TSESLint.RuleModule<
|
|
14
|
+
MessageIds,
|
|
15
|
+
Options
|
|
16
|
+
> = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
'Replace `xs.length >= min && xs.length <= max` with `Arr.isBoundedLengthArray(xs, min, max)` from ts-data-forge.',
|
|
22
|
+
},
|
|
23
|
+
fixable: 'code',
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
useIsBoundedLengthArray:
|
|
27
|
+
'Replace `{{original}}` with `Arr.isBoundedLengthArray({{arrayName}}, {{min}}, {{max}})` from ts-data-forge.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create: (context) => {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
|
|
34
|
+
const program = sourceCode.ast;
|
|
35
|
+
|
|
36
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
37
|
+
|
|
38
|
+
const services = context.sourceCode.parserServices;
|
|
39
|
+
|
|
40
|
+
const mut_nodesToFix: {
|
|
41
|
+
node: TSESTree.LogicalExpression;
|
|
42
|
+
arrayExpression: TSESTree.Expression;
|
|
43
|
+
minExpression: TSESTree.Expression;
|
|
44
|
+
maxExpression: TSESTree.Expression;
|
|
45
|
+
}[] = [];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
LogicalExpression: (node) => {
|
|
49
|
+
// Only `a && b` can encode a `min && max` length range.
|
|
50
|
+
if (node.operator !== '&&') return;
|
|
51
|
+
|
|
52
|
+
const left = parseLengthComparison(node.left, sourceCode);
|
|
53
|
+
|
|
54
|
+
const right = parseLengthComparison(node.right, sourceCode);
|
|
55
|
+
|
|
56
|
+
if (left === undefined || right === undefined) return;
|
|
57
|
+
|
|
58
|
+
// Need one lower bound and one upper bound on the same array.
|
|
59
|
+
if (left.kind === right.kind) return;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
sourceCode.getText(left.array) !== sourceCode.getText(right.array)
|
|
63
|
+
) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const min = left.kind === 'min' ? left : right;
|
|
68
|
+
|
|
69
|
+
const max = left.kind === 'min' ? right : left;
|
|
70
|
+
|
|
71
|
+
const arrayExpression = min.array;
|
|
72
|
+
|
|
73
|
+
// Check if arrayExpression is actually an array type
|
|
74
|
+
if (services?.program !== undefined && services.program !== null) {
|
|
75
|
+
const checker = services.program.getTypeChecker();
|
|
76
|
+
|
|
77
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
|
|
78
|
+
|
|
79
|
+
if (tsNode !== undefined) {
|
|
80
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
81
|
+
|
|
82
|
+
// Check if it's an array type or tuple type
|
|
83
|
+
const isArrayType =
|
|
84
|
+
checker.isArrayType(type) || checker.isTupleType(type);
|
|
85
|
+
|
|
86
|
+
if (!isArrayType) return;
|
|
87
|
+
} else {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mut_nodesToFix.push({
|
|
95
|
+
node,
|
|
96
|
+
arrayExpression,
|
|
97
|
+
minExpression: min.bound,
|
|
98
|
+
maxExpression: max.bound,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
'Program:exit': () => {
|
|
102
|
+
const namedImports = getNamedImports(tsDataForgeImport);
|
|
103
|
+
|
|
104
|
+
const hasArrImport = namedImports.includes('Arr');
|
|
105
|
+
|
|
106
|
+
for (const [
|
|
107
|
+
index,
|
|
108
|
+
{ node, arrayExpression, minExpression, maxExpression },
|
|
109
|
+
] of mut_nodesToFix.entries()) {
|
|
110
|
+
const arrayText = sourceCode.getText(arrayExpression);
|
|
111
|
+
|
|
112
|
+
const minText = sourceCode.getText(minExpression);
|
|
113
|
+
|
|
114
|
+
const maxText = sourceCode.getText(maxExpression);
|
|
115
|
+
|
|
116
|
+
const originalText = sourceCode.getText(node);
|
|
117
|
+
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'useIsBoundedLengthArray',
|
|
121
|
+
data: {
|
|
122
|
+
original: originalText,
|
|
123
|
+
arrayName: arrayText,
|
|
124
|
+
min: minText,
|
|
125
|
+
max: maxText,
|
|
126
|
+
},
|
|
127
|
+
fix: (fixer) => {
|
|
128
|
+
const replacement = `Arr.isBoundedLengthArray(${arrayText}, ${minText}, ${maxText})`;
|
|
129
|
+
|
|
130
|
+
const importFixes =
|
|
131
|
+
index === 0 && !hasArrImport
|
|
132
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
133
|
+
: [];
|
|
134
|
+
|
|
135
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
defaultOptions: [],
|
|
143
|
+
} as const;
|