eslint-plugin-ts-data-forge 0.10.5 → 0.10.7
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 +3 -3
- package/dist/plugin.d.mts +2 -2
- 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 +5 -5
- package/dist/rules/ast-utils.d.mts +8 -9
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +3 -4
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-binding.d.mts +38 -0
- package/dist/rules/import-binding.d.mts.map +1 -0
- package/dist/rules/import-binding.mjs +53 -0
- package/dist/rules/import-binding.mjs.map +1 -0
- package/dist/rules/mutable-result.mjs +37 -37
- package/dist/rules/mutable-result.mjs.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +2 -2
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
- package/dist/rules/prefer-arr-generate.mjs +146 -136
- package/dist/rules/prefer-arr-generate.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +9 -18
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +16 -21
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +38 -43
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +19 -26
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +49 -16
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.d.mts +2 -2
- package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
- package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +18 -24
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +2 -2
- package/package.json +6 -6
- package/src/plugin.mts +4 -3
- package/src/rule-types.mts +5 -5
- package/src/rules/ast-utils.mts +14 -15
- package/src/rules/import-binding.mts +94 -0
- package/src/rules/mutable-result.mts +56 -56
- package/src/rules/no-unnecessary-type-guard.mts +203 -207
- package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
- package/src/rules/prefer-arr-generate.mts +222 -214
- package/src/rules/prefer-arr-generate.test.mts +490 -133
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-max-length-array.mts +23 -36
- package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-min-length-array.mts +23 -36
- package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-non-empty.mts +17 -34
- package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
- package/src/rules/prefer-arr-scan.mts +17 -42
- package/src/rules/prefer-arr-scan.test.mts +55 -0
- package/src/rules/prefer-arr-sum.mts +22 -36
- package/src/rules/prefer-arr-uniq.mts +51 -62
- package/src/rules/prefer-arr-uniq.test.mts +46 -20
- package/src/rules/prefer-as-int.mts +27 -52
- package/src/rules/prefer-as-int.test.mts +79 -29
- package/src/rules/prefer-canonical-array-slicing.mts +101 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
- package/src/rules/prefer-canonical-length-cast.mts +29 -29
- package/src/rules/prefer-canonical-length-guard.mts +2 -2
- package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
- package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
- package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
- package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
- package/src/rules/prefer-pipe-map-nullable.mts +17 -19
- package/src/rules/prefer-range-for-loop.mts +37 -61
- package/src/rules/prefer-range-for-loop.test.mts +2 -2
|
@@ -167,3 +167,113 @@ describe('prefer-arr-is-non-empty with parenthesized operands', () => {
|
|
|
167
167
|
],
|
|
168
168
|
});
|
|
169
169
|
}, 20000);
|
|
170
|
+
|
|
171
|
+
describe('prefer-arr-is-non-empty and the import it adds', () => {
|
|
172
|
+
tester.run('prefer-arr-is-non-empty', preferArrIsNonEmpty, {
|
|
173
|
+
valid: [],
|
|
174
|
+
invalid: [
|
|
175
|
+
{
|
|
176
|
+
name: 'a leading directive stays first',
|
|
177
|
+
code: dedent`
|
|
178
|
+
'use client';
|
|
179
|
+
declare const xs: readonly number[];
|
|
180
|
+
const ok = xs.length > 0;
|
|
181
|
+
`,
|
|
182
|
+
output: dedent`
|
|
183
|
+
'use client';
|
|
184
|
+
import { Arr } from 'ts-data-forge';
|
|
185
|
+
declare const xs: readonly number[];
|
|
186
|
+
const ok = Arr.isNonEmpty(xs);
|
|
187
|
+
`,
|
|
188
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'joins an existing named import from the module',
|
|
192
|
+
code: dedent`
|
|
193
|
+
import { asInt } from 'ts-data-forge';
|
|
194
|
+
declare const xs: readonly number[];
|
|
195
|
+
const ok = xs.length > 0;
|
|
196
|
+
`,
|
|
197
|
+
output: dedent`
|
|
198
|
+
import { asInt, Arr } from 'ts-data-forge';
|
|
199
|
+
declare const xs: readonly number[];
|
|
200
|
+
const ok = Arr.isNonEmpty(xs);
|
|
201
|
+
`,
|
|
202
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'turns a type-only specifier into a value import',
|
|
206
|
+
code: dedent`
|
|
207
|
+
import { type Arr } from 'ts-data-forge';
|
|
208
|
+
declare const xs: readonly number[];
|
|
209
|
+
const ok = xs.length > 0;
|
|
210
|
+
`,
|
|
211
|
+
output: dedent`
|
|
212
|
+
import { Arr } from 'ts-data-forge';
|
|
213
|
+
declare const xs: readonly number[];
|
|
214
|
+
const ok = Arr.isNonEmpty(xs);
|
|
215
|
+
`,
|
|
216
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'writes the local name of an aliased import',
|
|
220
|
+
code: dedent`
|
|
221
|
+
import { Arr as A } from 'ts-data-forge';
|
|
222
|
+
declare const xs: readonly number[];
|
|
223
|
+
const ok = xs.length > 0;
|
|
224
|
+
`,
|
|
225
|
+
output: dedent`
|
|
226
|
+
import { Arr as A } from 'ts-data-forge';
|
|
227
|
+
declare const xs: readonly number[];
|
|
228
|
+
const ok = A.isNonEmpty(xs);
|
|
229
|
+
`,
|
|
230
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'no fix when an import type binds the name',
|
|
234
|
+
code: dedent`
|
|
235
|
+
import type { Arr } from 'ts-data-forge';
|
|
236
|
+
declare const xs: readonly number[];
|
|
237
|
+
const ok = xs.length > 0;
|
|
238
|
+
`,
|
|
239
|
+
output: null,
|
|
240
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'no fix where a local binding takes the name',
|
|
244
|
+
code: dedent`
|
|
245
|
+
declare const xs: readonly number[];
|
|
246
|
+
const f = (Arr: unknown) => xs.length > 0;
|
|
247
|
+
`,
|
|
248
|
+
output: null,
|
|
249
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'each fix adds the import it needs, one per pass',
|
|
253
|
+
code: dedent`
|
|
254
|
+
declare const xs: readonly number[];
|
|
255
|
+
const a = xs.length > 0;
|
|
256
|
+
const b = 0 < xs.length;
|
|
257
|
+
`,
|
|
258
|
+
output: [
|
|
259
|
+
dedent`
|
|
260
|
+
import { Arr } from 'ts-data-forge';
|
|
261
|
+
declare const xs: readonly number[];
|
|
262
|
+
const a = Arr.isNonEmpty(xs);
|
|
263
|
+
const b = 0 < xs.length;
|
|
264
|
+
`,
|
|
265
|
+
dedent`
|
|
266
|
+
import { Arr } from 'ts-data-forge';
|
|
267
|
+
declare const xs: readonly number[];
|
|
268
|
+
const a = Arr.isNonEmpty(xs);
|
|
269
|
+
const b = Arr.isNonEmpty(xs);
|
|
270
|
+
`,
|
|
271
|
+
],
|
|
272
|
+
errors: [
|
|
273
|
+
{ messageId: 'useIsNonEmpty' },
|
|
274
|
+
{ messageId: 'useIsNonEmpty' },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
}, 20000);
|
|
@@ -4,17 +4,14 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
7
|
getArgumentText,
|
|
9
|
-
getImportDeclarations,
|
|
10
|
-
getNamedImports,
|
|
11
8
|
skipTypeWrappers,
|
|
12
9
|
typeWrapperLayers,
|
|
13
10
|
} from 'eslint-rule-forge';
|
|
14
11
|
import { Arr } from 'ts-data-forge';
|
|
15
12
|
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
16
13
|
import * as ts from 'typescript';
|
|
17
|
-
import {
|
|
14
|
+
import { tsDataForgeValueAt } from './import-binding.mjs';
|
|
18
15
|
|
|
19
16
|
type Options = readonly [];
|
|
20
17
|
|
|
@@ -47,8 +44,9 @@ type SliceCall = Readonly<{
|
|
|
47
44
|
type AccumulationCandidate = Readonly<{
|
|
48
45
|
call: TSESTree.CallExpression;
|
|
49
46
|
/**
|
|
50
|
-
* What the call is replaced with,
|
|
51
|
-
*
|
|
47
|
+
* What the call is replaced with, after the name `Arr` is bound to
|
|
48
|
+
* (`scan(xs, f, init)`), or `undefined` when the rewrite could not be derived
|
|
49
|
+
* and the report carries no fix.
|
|
52
50
|
*/
|
|
53
51
|
replacement: string | undefined;
|
|
54
52
|
}>;
|
|
@@ -180,13 +178,6 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
180
178
|
create: (context) => {
|
|
181
179
|
const sourceCode = context.sourceCode;
|
|
182
180
|
|
|
183
|
-
const program = sourceCode.ast;
|
|
184
|
-
|
|
185
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
186
|
-
program,
|
|
187
|
-
TS_DATA_FORGE_MODULE,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
181
|
const services = sourceCode.parserServices;
|
|
191
182
|
|
|
192
183
|
const checker = services?.program?.getTypeChecker();
|
|
@@ -289,12 +280,12 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
289
280
|
// `acc.at(-1)` is `T | undefined` only because `at` can go out of bounds,
|
|
290
281
|
// which a seeded accumulator never does. So once the value `scan` carries
|
|
291
282
|
// is known not to be nullish, a `?? fallback` guarding the read is dead
|
|
292
|
-
// and the fix takes it with the read — leaving it would hand the author
|
|
293
|
-
// unnecessary condition that no rule can fix for them. Every layer of
|
|
294
|
-
// type wrappers is asked (see typeWrapperLayers): the value under an
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
283
|
+
// and the fix takes it with the read — leaving it would hand the author
|
|
284
|
+
// an unnecessary condition that no rule can fix for them. Every layer of
|
|
285
|
+
// type wrappers is asked (see typeWrapperLayers): the value under an `as`
|
|
286
|
+
// / `!` is what `scan` carries at run time (`[init!]` may still hold
|
|
287
|
+
// `undefined`), and a wider type a wrapper states is the author saying it
|
|
288
|
+
// may be nullish.
|
|
298
289
|
const carriesNullish = [seed, element].some((node) =>
|
|
299
290
|
typeWrapperLayers(node).some(isNullish),
|
|
300
291
|
);
|
|
@@ -363,7 +354,7 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
363
354
|
? `(${elementText})`
|
|
364
355
|
: elementText;
|
|
365
356
|
|
|
366
|
-
return `
|
|
357
|
+
return `scan(${arrayText}, (${parameterText}) => ${bodyText}, ${getArgumentText(sourceCode, seed)})`;
|
|
367
358
|
};
|
|
368
359
|
|
|
369
360
|
/**
|
|
@@ -498,36 +489,20 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
498
489
|
// been seen by the time its `map` is judged, whichever order the traversal
|
|
499
490
|
// reached them in.
|
|
500
491
|
'Program:exit': () => {
|
|
501
|
-
const
|
|
502
|
-
getNamedImports(tsDataForgeImports).includes('Arr');
|
|
503
|
-
|
|
504
|
-
// The import is inserted once per pass: two fixes writing the same
|
|
505
|
-
// insertion point would otherwise produce it twice.
|
|
506
|
-
const mut_importNeededBy = hasArrImport
|
|
507
|
-
? undefined
|
|
508
|
-
: mut_accumulations.find(
|
|
509
|
-
(accumulation) => accumulation.replacement !== undefined,
|
|
510
|
-
)?.call;
|
|
492
|
+
const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
|
|
511
493
|
|
|
512
|
-
for (const
|
|
513
|
-
const
|
|
494
|
+
for (const { call, replacement } of mut_accumulations) {
|
|
495
|
+
const arr = arrAt(call);
|
|
514
496
|
|
|
515
497
|
context.report({
|
|
516
498
|
node: call,
|
|
517
499
|
messageId: 'preferArrScanAccumulate',
|
|
518
500
|
fix:
|
|
519
|
-
replacement === undefined
|
|
501
|
+
replacement === undefined || arr === undefined
|
|
520
502
|
? undefined
|
|
521
503
|
: (fixer) => [
|
|
522
|
-
...(
|
|
523
|
-
|
|
524
|
-
fixer,
|
|
525
|
-
program,
|
|
526
|
-
TS_DATA_FORGE_MODULE,
|
|
527
|
-
['Arr'],
|
|
528
|
-
)
|
|
529
|
-
: []),
|
|
530
|
-
fixer.replaceText(call, replacement),
|
|
504
|
+
...arr.edits(fixer),
|
|
505
|
+
fixer.replaceText(call, `${arr.localName}.${replacement}`),
|
|
531
506
|
],
|
|
532
507
|
});
|
|
533
508
|
}
|
|
@@ -442,3 +442,58 @@ describe('prefer-arr-scan with parenthesized operands', () => {
|
|
|
442
442
|
],
|
|
443
443
|
});
|
|
444
444
|
});
|
|
445
|
+
|
|
446
|
+
describe('prefer-arr-scan and the import it adds', () => {
|
|
447
|
+
tester.run('prefer-arr-scan', preferArrScan, {
|
|
448
|
+
valid: [],
|
|
449
|
+
invalid: [
|
|
450
|
+
{
|
|
451
|
+
name: 'writes the local name of an aliased import',
|
|
452
|
+
code: dedent`
|
|
453
|
+
import { Arr as A } from 'ts-data-forge';
|
|
454
|
+
declare const xs: readonly number[];
|
|
455
|
+
const totals = xs.reduce<readonly number[]>(
|
|
456
|
+
(acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
|
|
457
|
+
[0],
|
|
458
|
+
);
|
|
459
|
+
`,
|
|
460
|
+
output: dedent`
|
|
461
|
+
import { Arr as A } from 'ts-data-forge';
|
|
462
|
+
declare const xs: readonly number[];
|
|
463
|
+
const totals = A.scan(xs, (acc, x) => (acc) + x, 0);
|
|
464
|
+
`,
|
|
465
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'turns a type-only specifier into a value import',
|
|
469
|
+
code: dedent`
|
|
470
|
+
import { type Arr } from 'ts-data-forge';
|
|
471
|
+
declare const xs: readonly number[];
|
|
472
|
+
const totals = xs.reduce<readonly number[]>(
|
|
473
|
+
(acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
|
|
474
|
+
[0],
|
|
475
|
+
);
|
|
476
|
+
`,
|
|
477
|
+
output: dedent`
|
|
478
|
+
import { Arr } from 'ts-data-forge';
|
|
479
|
+
declare const xs: readonly number[];
|
|
480
|
+
const totals = Arr.scan(xs, (acc, x) => (acc) + x, 0);
|
|
481
|
+
`,
|
|
482
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'no fix when an import type binds the name',
|
|
486
|
+
code: dedent`
|
|
487
|
+
import type { Arr } from 'ts-data-forge';
|
|
488
|
+
declare const xs: readonly number[];
|
|
489
|
+
const totals = xs.reduce<readonly number[]>(
|
|
490
|
+
(acc, x) => [...acc, (acc.at(-1) ?? 0) + x],
|
|
491
|
+
[0],
|
|
492
|
+
);
|
|
493
|
+
`,
|
|
494
|
+
output: null,
|
|
495
|
+
errors: [{ messageId: 'preferArrScanAccumulate' }],
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
});
|
|
499
|
+
});
|
|
@@ -4,16 +4,13 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
7
|
getArgumentText,
|
|
9
|
-
getImportDeclarations,
|
|
10
|
-
getNamedImports,
|
|
11
8
|
skipTypeWrappers,
|
|
12
9
|
typeWrapperLayers,
|
|
13
10
|
} from 'eslint-rule-forge';
|
|
14
11
|
import { Arr, pipe } from 'ts-data-forge';
|
|
15
12
|
import type { TypeReference } from 'typescript';
|
|
16
|
-
import {
|
|
13
|
+
import { tsDataForgeValueAt } from './import-binding.mjs';
|
|
17
14
|
|
|
18
15
|
type Options = readonly [];
|
|
19
16
|
|
|
@@ -39,13 +36,6 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
39
36
|
create: (context) => {
|
|
40
37
|
const sourceCode = context.sourceCode;
|
|
41
38
|
|
|
42
|
-
const program = sourceCode.ast;
|
|
43
|
-
|
|
44
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
45
|
-
program,
|
|
46
|
-
TS_DATA_FORGE_MODULE,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
39
|
const services = context.sourceCode.parserServices;
|
|
50
40
|
|
|
51
41
|
const mut_nodesToFix: {
|
|
@@ -77,9 +67,9 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
77
67
|
|
|
78
68
|
const reducer = node.arguments[0];
|
|
79
69
|
|
|
80
|
-
// The initial value and the operands of the body are read through
|
|
81
|
-
//
|
|
82
|
-
//
|
|
70
|
+
// The initial value and the operands of the body are read through type
|
|
71
|
+
// wrappers: `0 as number` is still 0, `(b satisfies number)` is still
|
|
72
|
+
// `b`. Types are asked of the values underneath too.
|
|
83
73
|
const initialValue = skipTypeWrappers(node.arguments[1]);
|
|
84
74
|
|
|
85
75
|
// Check initial value is 0
|
|
@@ -267,16 +257,16 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
267
257
|
}
|
|
268
258
|
},
|
|
269
259
|
'Program:exit': () => {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const hasArrImport = namedImports.includes('Arr');
|
|
260
|
+
const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
|
|
273
261
|
|
|
274
|
-
for (const
|
|
262
|
+
for (const nodeInfo of mut_nodesToFix) {
|
|
275
263
|
const arrayText = getArgumentText(
|
|
276
264
|
sourceCode,
|
|
277
265
|
nodeInfo.arrayExpression,
|
|
278
266
|
);
|
|
279
267
|
|
|
268
|
+
const arr = arrAt(nodeInfo.node);
|
|
269
|
+
|
|
280
270
|
context.report({
|
|
281
271
|
node: nodeInfo.node,
|
|
282
272
|
messageId: nodeInfo.messageId,
|
|
@@ -284,24 +274,20 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
284
274
|
arrayName: arrayText,
|
|
285
275
|
mapper: nodeInfo.mapper ?? '',
|
|
286
276
|
},
|
|
287
|
-
fix:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
...importFixes,
|
|
302
|
-
fixer.replaceText(nodeInfo.node, replacement),
|
|
303
|
-
];
|
|
304
|
-
},
|
|
277
|
+
fix:
|
|
278
|
+
arr === undefined
|
|
279
|
+
? null
|
|
280
|
+
: (fixer) => {
|
|
281
|
+
const replacement =
|
|
282
|
+
nodeInfo.messageId === 'useArrSum'
|
|
283
|
+
? `${arr.localName}.sum(${arrayText})`
|
|
284
|
+
: `${arr.localName}.sumBy(${arrayText}, ${nodeInfo.mapper})`;
|
|
285
|
+
|
|
286
|
+
return [
|
|
287
|
+
...arr.edits(fixer),
|
|
288
|
+
fixer.replaceText(nodeInfo.node, replacement),
|
|
289
|
+
];
|
|
290
|
+
},
|
|
305
291
|
});
|
|
306
292
|
}
|
|
307
293
|
},
|
|
@@ -5,16 +5,14 @@ import {
|
|
|
5
5
|
type TSESTree,
|
|
6
6
|
} from '@typescript-eslint/utils';
|
|
7
7
|
import {
|
|
8
|
-
buildNamedImportFix,
|
|
9
8
|
getArgumentText,
|
|
10
|
-
getImportDeclarations,
|
|
11
|
-
getImportedLocalName,
|
|
12
9
|
skipTypeWrappers,
|
|
13
10
|
typeWrapperLayers,
|
|
14
11
|
} from 'eslint-rule-forge';
|
|
15
12
|
import { Arr } from 'ts-data-forge';
|
|
16
13
|
import * as ts from 'typescript';
|
|
17
|
-
import { primitiveTypeFlags
|
|
14
|
+
import { primitiveTypeFlags } from './constants.mjs';
|
|
15
|
+
import { tsDataForgeValueAt } from './import-binding.mjs';
|
|
18
16
|
import { needsMutableResult } from './mutable-result.mjs';
|
|
19
17
|
|
|
20
18
|
type Options = readonly [];
|
|
@@ -51,21 +49,14 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
51
49
|
create: (context) => {
|
|
52
50
|
const sourceCode = context.sourceCode;
|
|
53
51
|
|
|
54
|
-
const program = sourceCode.ast;
|
|
55
|
-
|
|
56
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
57
|
-
program,
|
|
58
|
-
TS_DATA_FORGE_MODULE,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
52
|
const services = sourceCode.parserServices;
|
|
62
53
|
|
|
63
54
|
const checker = services?.program?.getTypeChecker();
|
|
64
55
|
|
|
65
56
|
const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
|
|
66
57
|
|
|
67
|
-
// Without type information the rule cannot tell an array of primitives
|
|
68
|
-
//
|
|
58
|
+
// Without type information the rule cannot tell an array of primitives from
|
|
59
|
+
// an array of objects, and would suggest code that does not compile.
|
|
69
60
|
if (checker === undefined || esTreeNodeToTSNodeMap === undefined) {
|
|
70
61
|
return {};
|
|
71
62
|
}
|
|
@@ -159,14 +150,7 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
159
150
|
check(node, element.argument);
|
|
160
151
|
},
|
|
161
152
|
'Program:exit': () => {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const arrName = arrLocalName ?? 'Arr';
|
|
165
|
-
|
|
166
|
-
// The import is added by the first fix only, so that several fixes do
|
|
167
|
-
// not insert it at the same position. When the first match is not
|
|
168
|
-
// autofixable, the first autofixable one carries it instead.
|
|
169
|
-
const firstAutofixable = mut_matches.find((m) => m.autofixable);
|
|
153
|
+
const arrAt = tsDataForgeValueAt(sourceCode, 'Arr');
|
|
170
154
|
|
|
171
155
|
for (const match of mut_matches) {
|
|
172
156
|
const arrayText = sourceCode.getText(match.arrayExpression);
|
|
@@ -176,16 +160,18 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
176
160
|
match.arrayExpression,
|
|
177
161
|
);
|
|
178
162
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
163
|
+
const arr = arrAt(match.node);
|
|
164
|
+
|
|
165
|
+
const fix: TSESLint.ReportFixFunction | null =
|
|
166
|
+
arr === undefined
|
|
167
|
+
? null
|
|
168
|
+
: (fixer) => [
|
|
169
|
+
...arr.edits(fixer),
|
|
170
|
+
fixer.replaceText(
|
|
171
|
+
match.node,
|
|
172
|
+
`${arr.localName}.uniq(${argumentText})`,
|
|
173
|
+
),
|
|
174
|
+
];
|
|
189
175
|
|
|
190
176
|
const data = { arrayName: arrayText } as const;
|
|
191
177
|
|
|
@@ -194,20 +180,23 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
194
180
|
node: match.node,
|
|
195
181
|
messageId: 'useArrUniq',
|
|
196
182
|
data,
|
|
197
|
-
fix
|
|
183
|
+
fix,
|
|
198
184
|
});
|
|
199
185
|
} else {
|
|
200
186
|
context.report({
|
|
201
187
|
node: match.node,
|
|
202
188
|
messageId: 'useArrUniq',
|
|
203
189
|
data,
|
|
204
|
-
suggest:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
190
|
+
suggest:
|
|
191
|
+
fix === null
|
|
192
|
+
? []
|
|
193
|
+
: [
|
|
194
|
+
{
|
|
195
|
+
messageId: 'suggestArrUniq',
|
|
196
|
+
data,
|
|
197
|
+
fix,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
211
200
|
});
|
|
212
201
|
}
|
|
213
202
|
}
|
|
@@ -257,6 +246,29 @@ const isGlobal = (
|
|
|
257
246
|
return variable === null || variable.defs.length === 0;
|
|
258
247
|
};
|
|
259
248
|
|
|
249
|
+
const resolveConstraint = (
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
251
|
+
checker: ts.TypeChecker,
|
|
252
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
253
|
+
type: ts.Type,
|
|
254
|
+
): ts.Type =>
|
|
255
|
+
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
256
|
+
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
257
|
+
: type;
|
|
258
|
+
|
|
259
|
+
const isPrimitiveType = (
|
|
260
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
261
|
+
checker: ts.TypeChecker,
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
263
|
+
type: ts.Type,
|
|
264
|
+
): boolean => {
|
|
265
|
+
const resolved = resolveConstraint(checker, type);
|
|
266
|
+
|
|
267
|
+
return resolved.isUnion()
|
|
268
|
+
? resolved.types.every((t) => isPrimitiveType(checker, t))
|
|
269
|
+
: (resolved.flags & primitiveTypeFlags) !== 0;
|
|
270
|
+
};
|
|
271
|
+
|
|
260
272
|
/**
|
|
261
273
|
* Whether `type` is an array or tuple (or a union or intersection of them) whose elements are
|
|
262
274
|
* all primitives, i.e. something `Arr.uniq` accepts.
|
|
@@ -291,26 +303,3 @@ const isPrimitiveArrayType = (
|
|
|
291
303
|
// An empty tuple has nothing to deduplicate, but is still accepted.
|
|
292
304
|
return typeArguments.every((t) => isPrimitiveType(checker, t));
|
|
293
305
|
};
|
|
294
|
-
|
|
295
|
-
const isPrimitiveType = (
|
|
296
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
297
|
-
checker: ts.TypeChecker,
|
|
298
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
299
|
-
type: ts.Type,
|
|
300
|
-
): boolean => {
|
|
301
|
-
const resolved = resolveConstraint(checker, type);
|
|
302
|
-
|
|
303
|
-
return resolved.isUnion()
|
|
304
|
-
? resolved.types.every((t) => isPrimitiveType(checker, t))
|
|
305
|
-
: (resolved.flags & primitiveTypeFlags) !== 0;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const resolveConstraint = (
|
|
309
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
310
|
-
checker: ts.TypeChecker,
|
|
311
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
312
|
-
type: ts.Type,
|
|
313
|
-
): ts.Type =>
|
|
314
|
-
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
315
|
-
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
316
|
-
: type;
|
|
@@ -188,12 +188,20 @@ describe('prefer-arr-uniq', () => {
|
|
|
188
188
|
const ys = [...new Set(xs)];
|
|
189
189
|
const zs = Array.from(new Set(xs));
|
|
190
190
|
`,
|
|
191
|
-
output:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
output: [
|
|
192
|
+
dedent`
|
|
193
|
+
import { Arr } from 'ts-data-forge';
|
|
194
|
+
const xs = ['a', 'b'];
|
|
195
|
+
const ys = Arr.uniq(xs);
|
|
196
|
+
const zs = Array.from(new Set(xs));
|
|
197
|
+
`,
|
|
198
|
+
dedent`
|
|
199
|
+
import { Arr } from 'ts-data-forge';
|
|
200
|
+
const xs = ['a', 'b'];
|
|
201
|
+
const ys = Arr.uniq(xs);
|
|
202
|
+
const zs = Arr.uniq(xs);
|
|
203
|
+
`,
|
|
204
|
+
],
|
|
197
205
|
errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
|
|
198
206
|
},
|
|
199
207
|
{
|
|
@@ -205,14 +213,24 @@ describe('prefer-arr-uniq', () => {
|
|
|
205
213
|
const b = [...new Set(u)];
|
|
206
214
|
const c = Array.from(new Set([1, 2, 1]));
|
|
207
215
|
`,
|
|
208
|
-
output:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
output: [
|
|
217
|
+
dedent`
|
|
218
|
+
import { Arr } from 'ts-data-forge';
|
|
219
|
+
const t = [1, 'a', 1] as const;
|
|
220
|
+
const u: readonly ('x' | 'y' | null | undefined | boolean | bigint)[] = [];
|
|
221
|
+
const a = Arr.uniq(t);
|
|
222
|
+
const b = [...new Set(u)];
|
|
223
|
+
const c = Array.from(new Set([1, 2, 1]));
|
|
224
|
+
`,
|
|
225
|
+
dedent`
|
|
226
|
+
import { Arr } from 'ts-data-forge';
|
|
227
|
+
const t = [1, 'a', 1] as const;
|
|
228
|
+
const u: readonly ('x' | 'y' | null | undefined | boolean | bigint)[] = [];
|
|
229
|
+
const a = Arr.uniq(t);
|
|
230
|
+
const b = Arr.uniq(u);
|
|
231
|
+
const c = Arr.uniq([1, 2, 1]);
|
|
232
|
+
`,
|
|
233
|
+
],
|
|
216
234
|
errors: [
|
|
217
235
|
{ messageId: 'useArrUniq' },
|
|
218
236
|
{ messageId: 'useArrUniq' },
|
|
@@ -465,12 +483,20 @@ describe('prefer-arr-uniq through type wrappers', () => {
|
|
|
465
483
|
const a = Array.from(new Set(xs) as ReadonlySet<string>);
|
|
466
484
|
const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
|
|
467
485
|
`,
|
|
468
|
-
output:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
486
|
+
output: [
|
|
487
|
+
dedent`
|
|
488
|
+
import { Arr } from 'ts-data-forge';
|
|
489
|
+
declare const xs: readonly string[];
|
|
490
|
+
const a = Arr.uniq(xs);
|
|
491
|
+
const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
|
|
492
|
+
`,
|
|
493
|
+
dedent`
|
|
494
|
+
import { Arr } from 'ts-data-forge';
|
|
495
|
+
declare const xs: readonly string[];
|
|
496
|
+
const a = Arr.uniq(xs);
|
|
497
|
+
const b = Arr.uniq(xs);
|
|
498
|
+
`,
|
|
499
|
+
],
|
|
474
500
|
errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
|
|
475
501
|
},
|
|
476
502
|
{
|