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
|
@@ -406,13 +406,73 @@ describe('no-unnecessary-type-guard', () => {
|
|
|
406
406
|
const y = isNonNullish(x);
|
|
407
407
|
`,
|
|
408
408
|
output: dedent`
|
|
409
|
-
import { isNotNull } from 'ts-data-forge';
|
|
410
|
-
import { isNonNullish } from 'ts-data-forge';
|
|
409
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
411
410
|
declare const x: string | null;
|
|
412
411
|
const y = isNotNull(x);
|
|
413
412
|
`,
|
|
414
413
|
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
415
414
|
},
|
|
415
|
+
{
|
|
416
|
+
name: 'the message names an aliased replacement guard by its local name',
|
|
417
|
+
code: dedent`
|
|
418
|
+
import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
|
|
419
|
+
declare const x: string | null;
|
|
420
|
+
const y = isNonNullish(x);
|
|
421
|
+
`,
|
|
422
|
+
output: dedent`
|
|
423
|
+
import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
|
|
424
|
+
declare const x: string | null;
|
|
425
|
+
const y = inn(x);
|
|
426
|
+
`,
|
|
427
|
+
errors: [
|
|
428
|
+
{
|
|
429
|
+
messageId: 'replaceTypeGuard',
|
|
430
|
+
data: { from: 'isNonNullish', to: 'inn' },
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: 'several reports bind each replacement guard they write',
|
|
436
|
+
code: dedent`
|
|
437
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
438
|
+
declare const x: string | null;
|
|
439
|
+
declare const z: string | undefined;
|
|
440
|
+
const a = isNonNullish(x);
|
|
441
|
+
const b = isNonNullish(x);
|
|
442
|
+
const c = isNonNullish(z);
|
|
443
|
+
`,
|
|
444
|
+
output: [
|
|
445
|
+
dedent`
|
|
446
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
447
|
+
declare const x: string | null;
|
|
448
|
+
declare const z: string | undefined;
|
|
449
|
+
const a = isNotNull(x);
|
|
450
|
+
const b = isNonNullish(x);
|
|
451
|
+
const c = isNonNullish(z);
|
|
452
|
+
`,
|
|
453
|
+
dedent`
|
|
454
|
+
import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
|
|
455
|
+
declare const x: string | null;
|
|
456
|
+
declare const z: string | undefined;
|
|
457
|
+
const a = isNotNull(x);
|
|
458
|
+
const b = isNonNullish(x);
|
|
459
|
+
const c = isNotUndefined(z);
|
|
460
|
+
`,
|
|
461
|
+
dedent`
|
|
462
|
+
import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
|
|
463
|
+
declare const x: string | null;
|
|
464
|
+
declare const z: string | undefined;
|
|
465
|
+
const a = isNotNull(x);
|
|
466
|
+
const b = isNotNull(x);
|
|
467
|
+
const c = isNotUndefined(z);
|
|
468
|
+
`,
|
|
469
|
+
],
|
|
470
|
+
errors: [
|
|
471
|
+
{ messageId: 'replaceTypeGuard' },
|
|
472
|
+
{ messageId: 'replaceTypeGuard' },
|
|
473
|
+
{ messageId: 'replaceTypeGuard' },
|
|
474
|
+
],
|
|
475
|
+
},
|
|
416
476
|
{
|
|
417
477
|
name: 'isNullish that only matches undefined -> isUndefined',
|
|
418
478
|
code: dedent`
|
|
@@ -6,11 +6,8 @@ import {
|
|
|
6
6
|
type TSESTree,
|
|
7
7
|
} from '@typescript-eslint/utils';
|
|
8
8
|
import {
|
|
9
|
-
buildNamedImportFix,
|
|
10
9
|
createGetLayerTypes,
|
|
11
10
|
getArgumentText,
|
|
12
|
-
getImportDeclarations,
|
|
13
|
-
getImportedLocalName,
|
|
14
11
|
negationOf,
|
|
15
12
|
prefixedNegationOf,
|
|
16
13
|
skipTypeWrappers,
|
|
@@ -19,7 +16,7 @@ import {
|
|
|
19
16
|
import { Arr, hasKey, isRecord } from 'ts-data-forge';
|
|
20
17
|
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
21
18
|
import * as ts from 'typescript';
|
|
22
|
-
import {
|
|
19
|
+
import { tsDataForgeValuesAt } from './import-binding.mjs';
|
|
23
20
|
import { needsMutableResult } from './mutable-result.mjs';
|
|
24
21
|
|
|
25
22
|
type Options = readonly [];
|
|
@@ -34,8 +31,8 @@ type Match = Readonly<{
|
|
|
34
31
|
}>;
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
|
-
* The callback of `xs.flatMap((x) => (cond ? [a, b] : []))` decides per
|
|
38
|
-
*
|
|
34
|
+
* The callback of `xs.flatMap((x) => (cond ? [a, b] : []))` decides per element
|
|
35
|
+
* whether to keep it and what to keep. Written as a loop, that is
|
|
39
36
|
*
|
|
40
37
|
* ```ts
|
|
41
38
|
* Arr.generate(function* () {
|
|
@@ -106,13 +103,6 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
106
103
|
create: (context) => {
|
|
107
104
|
const sourceCode = context.sourceCode;
|
|
108
105
|
|
|
109
|
-
const program = sourceCode.ast;
|
|
110
|
-
|
|
111
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
112
|
-
program,
|
|
113
|
-
TS_DATA_FORGE_MODULE,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
106
|
const services = sourceCode.parserServices;
|
|
117
107
|
|
|
118
108
|
const tsProgram = services?.program ?? undefined;
|
|
@@ -132,8 +122,8 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
132
122
|
esTreeNodeToTSNodeMap,
|
|
133
123
|
});
|
|
134
124
|
|
|
135
|
-
// The negation is the test of the loop's `if`, where only its truthiness
|
|
136
|
-
//
|
|
125
|
+
// The negation is the test of the loop's `if`, where only its truthiness is
|
|
126
|
+
// read, so `!!a` may always be written `a`.
|
|
137
127
|
const negationContext: NegationContext = {
|
|
138
128
|
sourceCode,
|
|
139
129
|
checker,
|
|
@@ -146,9 +136,10 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
146
136
|
node: TSESTree.Node,
|
|
147
137
|
): ts.Node | undefined => esTreeNodeToTSNodeMap.get(node);
|
|
148
138
|
|
|
149
|
-
|
|
139
|
+
// The name the rewrite writes, and, at a node, the edits that bind it.
|
|
140
|
+
const arr = tsDataForgeValuesAt(sourceCode, ['Arr']);
|
|
150
141
|
|
|
151
|
-
const arrName =
|
|
142
|
+
const arrName = arr.localNames[0] ?? 'Arr';
|
|
152
143
|
|
|
153
144
|
const mut_matches: Match[] = [];
|
|
154
145
|
|
|
@@ -172,8 +163,8 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
172
163
|
|
|
173
164
|
const callback = node.arguments[0];
|
|
174
165
|
|
|
175
|
-
// A wrapper around the callback may be what types its parameter, so
|
|
176
|
-
//
|
|
166
|
+
// A wrapper around the callback may be what types its parameter, so a
|
|
167
|
+
// wrapped callback is not unwrapped.
|
|
177
168
|
if (
|
|
178
169
|
callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
179
170
|
callback.async ||
|
|
@@ -291,45 +282,53 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
291
282
|
),
|
|
292
283
|
true,
|
|
293
284
|
) &&
|
|
294
|
-
!losesComments(
|
|
285
|
+
!losesComments(
|
|
286
|
+
sourceCode,
|
|
287
|
+
node,
|
|
288
|
+
[
|
|
289
|
+
receiver.range,
|
|
290
|
+
param.range,
|
|
291
|
+
...(typeArguments === undefined ? [] : [typeArguments.range]),
|
|
292
|
+
],
|
|
293
|
+
shape,
|
|
294
|
+
) &&
|
|
295
295
|
!needsMutableResult(sourceCode, checker, getTsNode, node);
|
|
296
296
|
|
|
297
297
|
mut_matches.push({ node, replacement, autofixable });
|
|
298
298
|
},
|
|
299
299
|
'Program:exit': () => {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// autofixable, the first autofixable one carries it instead.
|
|
303
|
-
const firstAutofixable = mut_matches.find((m) => m.autofixable);
|
|
304
|
-
|
|
300
|
+
// Every fix brings `Arr` into scope itself, so that one applied alone
|
|
301
|
+
// (an editor's quick fix) is complete.
|
|
305
302
|
for (const match of mut_matches) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
];
|
|
303
|
+
const binding = arr.at(match.node);
|
|
304
|
+
|
|
305
|
+
const fix: TSESLint.ReportFixFunction | null =
|
|
306
|
+
binding === undefined
|
|
307
|
+
? null
|
|
308
|
+
: (fixer) => [
|
|
309
|
+
...binding.edits(fixer),
|
|
310
|
+
fixer.replaceText(match.node, match.replacement),
|
|
311
|
+
];
|
|
316
312
|
|
|
317
313
|
if (match.autofixable) {
|
|
318
314
|
context.report({
|
|
319
315
|
node: match.node,
|
|
320
316
|
messageId: 'useArrGenerate',
|
|
321
|
-
fix
|
|
317
|
+
fix,
|
|
322
318
|
});
|
|
323
319
|
} else {
|
|
324
320
|
context.report({
|
|
325
321
|
node: match.node,
|
|
326
322
|
messageId: 'useArrGenerate',
|
|
327
|
-
suggest:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
suggest:
|
|
324
|
+
fix === null
|
|
325
|
+
? []
|
|
326
|
+
: [
|
|
327
|
+
{
|
|
328
|
+
messageId: 'suggestArrGenerate',
|
|
329
|
+
fix,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
333
332
|
});
|
|
334
333
|
}
|
|
335
334
|
}
|
|
@@ -452,6 +451,63 @@ const getFilterMapShape = (
|
|
|
452
451
|
};
|
|
453
452
|
};
|
|
454
453
|
|
|
454
|
+
/** The whitespace at the start of the line that `index` is on. */
|
|
455
|
+
const lineIndentOf = (
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
457
|
+
sourceCode: TSESLint.SourceCode,
|
|
458
|
+
index: number,
|
|
459
|
+
): string => {
|
|
460
|
+
const { line } = sourceCode.getLocFromIndex(index);
|
|
461
|
+
|
|
462
|
+
const lineText = sourceCode.lines[line - 1] ?? '';
|
|
463
|
+
|
|
464
|
+
return /^\s*/u.exec(lineText)?.[0] ?? '';
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* `text`, the source at `range`, with every line after the first shifted from
|
|
469
|
+
* the indentation of the line `range` starts on to `targetIndent`. Left as it
|
|
470
|
+
* is when a template literal or string spans lines inside it, since shifting
|
|
471
|
+
* those lines would change the string.
|
|
472
|
+
*/
|
|
473
|
+
const reindent = (
|
|
474
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
475
|
+
sourceCode: TSESLint.SourceCode,
|
|
476
|
+
range: FixedLengthTuple<2, number>,
|
|
477
|
+
targetIndent: string,
|
|
478
|
+
text: string,
|
|
479
|
+
): string => {
|
|
480
|
+
if (!text.includes('\n')) {
|
|
481
|
+
return text;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const spansLines = sourceCode.ast.tokens.some(
|
|
485
|
+
(token) =>
|
|
486
|
+
range[0] <= token.range[0] &&
|
|
487
|
+
token.range[1] <= range[1] &&
|
|
488
|
+
(token.type === AST_TOKEN_TYPES.Template ||
|
|
489
|
+
token.type === AST_TOKEN_TYPES.String) &&
|
|
490
|
+
token.loc.start.line !== token.loc.end.line,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
if (spansLines) {
|
|
494
|
+
return text;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const sourceIndent = lineIndentOf(sourceCode, range[0]);
|
|
498
|
+
|
|
499
|
+
const [first, ...rest] = text.split('\n');
|
|
500
|
+
|
|
501
|
+
return [
|
|
502
|
+
first,
|
|
503
|
+
...rest.map((line) =>
|
|
504
|
+
line.startsWith(sourceIndent)
|
|
505
|
+
? `${targetIndent}${line.slice(sourceIndent.length)}`
|
|
506
|
+
: line,
|
|
507
|
+
),
|
|
508
|
+
].join('\n');
|
|
509
|
+
};
|
|
510
|
+
|
|
455
511
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
456
512
|
const buildReplacement = ({
|
|
457
513
|
sourceCode,
|
|
@@ -534,19 +590,29 @@ const buildReplacement = ({
|
|
|
534
590
|
};
|
|
535
591
|
|
|
536
592
|
/**
|
|
537
|
-
* Whether a comment inside `
|
|
538
|
-
* rewrite
|
|
593
|
+
* Whether a comment inside `node`, the call the fix replaces, lies outside
|
|
594
|
+
* every range the rewrite copies verbatim, and would therefore be dropped.
|
|
595
|
+
*
|
|
596
|
+
* `copied` lists those ranges besides the shape's: the receiver, the type
|
|
597
|
+
* arguments and the parameter. The test counts only when it is copied as it is:
|
|
598
|
+
* a negated test is rebuilt by `negationOf`, which can drop a comment (one
|
|
599
|
+
* between the operands of `a && b` is gone from `!a || !b`), so any comment in
|
|
600
|
+
* one keeps the fix a suggestion, even where the negation carries it over. A
|
|
601
|
+
* comment between the receiver and `.flatMap`, after `flatMap(` or after the
|
|
602
|
+
* callback is in none of the ranges.
|
|
539
603
|
*/
|
|
540
604
|
const losesComments = (
|
|
541
605
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
542
606
|
sourceCode: TSESLint.SourceCode,
|
|
543
607
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
544
|
-
|
|
608
|
+
node: TSESTree.CallExpression,
|
|
609
|
+
copied: readonly FixedLengthTuple<2, number>[],
|
|
545
610
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
546
611
|
shape: FilterMapShape,
|
|
547
612
|
): boolean => {
|
|
548
613
|
const keptRanges: readonly FixedLengthTuple<2, number>[] = [
|
|
549
|
-
|
|
614
|
+
...copied,
|
|
615
|
+
...(shape.negated ? ([] as const) : ([shape.test.range] as const)),
|
|
550
616
|
...shape.elements.map((element) => element.range),
|
|
551
617
|
...(shape.preludeRange === undefined
|
|
552
618
|
? ([] as const)
|
|
@@ -554,7 +620,7 @@ const losesComments = (
|
|
|
554
620
|
] as const;
|
|
555
621
|
|
|
556
622
|
return sourceCode
|
|
557
|
-
.getCommentsInside(
|
|
623
|
+
.getCommentsInside(node)
|
|
558
624
|
.some((comment) =>
|
|
559
625
|
keptRanges.every(
|
|
560
626
|
([start, end]) =>
|
|
@@ -563,63 +629,6 @@ const losesComments = (
|
|
|
563
629
|
);
|
|
564
630
|
};
|
|
565
631
|
|
|
566
|
-
/**
|
|
567
|
-
* `text`, the source at `range`, with every line after the first shifted from
|
|
568
|
-
* the indentation of the line `range` starts on to `targetIndent`. Left as it
|
|
569
|
-
* is when a template literal or string spans lines inside it, since shifting
|
|
570
|
-
* those lines would change the string.
|
|
571
|
-
*/
|
|
572
|
-
const reindent = (
|
|
573
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
574
|
-
sourceCode: TSESLint.SourceCode,
|
|
575
|
-
range: FixedLengthTuple<2, number>,
|
|
576
|
-
targetIndent: string,
|
|
577
|
-
text: string,
|
|
578
|
-
): string => {
|
|
579
|
-
if (!text.includes('\n')) {
|
|
580
|
-
return text;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const spansLines = sourceCode.ast.tokens.some(
|
|
584
|
-
(token) =>
|
|
585
|
-
range[0] <= token.range[0] &&
|
|
586
|
-
token.range[1] <= range[1] &&
|
|
587
|
-
(token.type === AST_TOKEN_TYPES.Template ||
|
|
588
|
-
token.type === AST_TOKEN_TYPES.String) &&
|
|
589
|
-
token.loc.start.line !== token.loc.end.line,
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
if (spansLines) {
|
|
593
|
-
return text;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const sourceIndent = lineIndentOf(sourceCode, range[0]);
|
|
597
|
-
|
|
598
|
-
const [first, ...rest] = text.split('\n');
|
|
599
|
-
|
|
600
|
-
return [
|
|
601
|
-
first,
|
|
602
|
-
...rest.map((line) =>
|
|
603
|
-
line.startsWith(sourceIndent)
|
|
604
|
-
? `${targetIndent}${line.slice(sourceIndent.length)}`
|
|
605
|
-
: line,
|
|
606
|
-
),
|
|
607
|
-
].join('\n');
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
/** The whitespace at the start of the line that `index` is on. */
|
|
611
|
-
const lineIndentOf = (
|
|
612
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
613
|
-
sourceCode: TSESLint.SourceCode,
|
|
614
|
-
index: number,
|
|
615
|
-
): string => {
|
|
616
|
-
const { line } = sourceCode.getLocFromIndex(index);
|
|
617
|
-
|
|
618
|
-
const lineText = sourceCode.lines[line - 1] ?? '';
|
|
619
|
-
|
|
620
|
-
return /^\s*/u.exec(lineText)?.[0] ?? '';
|
|
621
|
-
};
|
|
622
|
-
|
|
623
632
|
/** Whether `node`, with its type wrappers taken off, is a `….filter(…)` call. */
|
|
624
633
|
const isFilterCall = (
|
|
625
634
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -642,78 +651,30 @@ const isFilterCall = (
|
|
|
642
651
|
};
|
|
643
652
|
|
|
644
653
|
/**
|
|
645
|
-
* Whether `
|
|
646
|
-
*
|
|
647
|
-
* see. TypeScript keeps the narrowing of a `const`, and of a parameter or `let`
|
|
648
|
-
* that is never reassigned, inside a closure; it drops every other one,
|
|
649
|
-
* including any narrowing of a property.
|
|
654
|
+
* Whether `type` is an array or tuple, or a union of them, or an intersection
|
|
655
|
+
* with one in it (`readonly T[] & Brand`).
|
|
650
656
|
*/
|
|
651
|
-
const
|
|
652
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
653
|
-
sourceCode: TSESLint.SourceCode,
|
|
657
|
+
const isArrayOrTupleType = (
|
|
654
658
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
655
659
|
checker: ts.TypeChecker,
|
|
656
660
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
657
|
-
|
|
658
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
659
|
-
node: TSESTree.Node,
|
|
660
|
-
isReceiver: boolean,
|
|
661
|
+
type: ts.Type,
|
|
661
662
|
): boolean => {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
return false;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// A method about to be called (`xs.flat()`): its type is that of the method,
|
|
670
|
-
// which no narrowing of interest changes.
|
|
671
|
-
if (
|
|
672
|
-
node.parent.type === AST_NODE_TYPES.CallExpression &&
|
|
673
|
-
node.parent.callee === node
|
|
674
|
-
) {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
679
|
-
if (!isReference(node)) {
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
variable?.references.every(
|
|
687
|
-
(reference) => !reference.isWrite() || reference.init === true,
|
|
688
|
-
) === true
|
|
689
|
-
) {
|
|
690
|
-
return false;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const tsNode = getTsNode(node);
|
|
695
|
-
|
|
696
|
-
if (tsNode === undefined) {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const declaredType = getDeclaredType(checker, tsNode);
|
|
701
|
-
|
|
702
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
663
|
+
const resolved =
|
|
664
|
+
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
665
|
+
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
666
|
+
: type;
|
|
703
667
|
|
|
704
|
-
return (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
!(isReceiver && iteratesTheSame(checker, declaredType, type))
|
|
710
|
-
);
|
|
668
|
+
return resolved.isUnion()
|
|
669
|
+
? resolved.types.every((t) => isArrayOrTupleType(checker, t))
|
|
670
|
+
: resolved.isIntersection()
|
|
671
|
+
? resolved.types.some((t) => isArrayOrTupleType(checker, t))
|
|
672
|
+
: checker.isArrayType(resolved) || checker.isTupleType(resolved);
|
|
711
673
|
};
|
|
712
674
|
|
|
713
675
|
/**
|
|
714
|
-
* Whether two types are both arrays or tuples whose elements have one type,
|
|
715
|
-
*
|
|
716
|
-
* are.
|
|
676
|
+
* Whether two types are both arrays or tuples whose elements have one type, as
|
|
677
|
+
* the declared and the narrowed type of `xs` after `Arr.isNonEmpty(xs)` are.
|
|
717
678
|
*/
|
|
718
679
|
const iteratesTheSame = (
|
|
719
680
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -740,8 +701,8 @@ const iteratesTheSame = (
|
|
|
740
701
|
* `undefined` when there is none to compare with (an index read with no
|
|
741
702
|
* property behind it, which nothing narrows).
|
|
742
703
|
*
|
|
743
|
-
* A property's is read off the type of the object it is accessed on, which
|
|
744
|
-
*
|
|
704
|
+
* A property's is read off the type of the object it is accessed on, which also
|
|
705
|
+
* covers a property of a mapped type (`Readonly<…>`), whose symbol has no
|
|
745
706
|
* declaration of its own. A variable's is the type at its declaration
|
|
746
707
|
* (`checker.getTypeOfSymbol` would say the same, but is missing before
|
|
747
708
|
* TypeScript 5.1).
|
|
@@ -787,10 +748,97 @@ const getDeclaredType = (
|
|
|
787
748
|
: checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
788
749
|
};
|
|
789
750
|
|
|
751
|
+
/** Whether an identifier reads a binding, rather than naming a property or a key. */
|
|
752
|
+
const isReference = (
|
|
753
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
754
|
+
node: TSESTree.Identifier,
|
|
755
|
+
): boolean => {
|
|
756
|
+
const { parent } = node;
|
|
757
|
+
|
|
758
|
+
return !(
|
|
759
|
+
(parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
760
|
+
parent.property === node &&
|
|
761
|
+
!parent.computed) ||
|
|
762
|
+
(parent.type === AST_NODE_TYPES.Property &&
|
|
763
|
+
parent.key === node &&
|
|
764
|
+
!parent.computed &&
|
|
765
|
+
!parent.shorthand)
|
|
766
|
+
);
|
|
767
|
+
};
|
|
768
|
+
|
|
790
769
|
/**
|
|
791
|
-
* Whether
|
|
792
|
-
*
|
|
793
|
-
*
|
|
770
|
+
* Whether `node`, a reference or a member access, has a type here narrower than
|
|
771
|
+
* its declared one, by a narrowing that a function created here would not see.
|
|
772
|
+
* TypeScript keeps the narrowing of a `const`, and of a parameter or `let` that
|
|
773
|
+
* is never reassigned, inside a closure; it drops every other one, including
|
|
774
|
+
* any narrowing of a property.
|
|
775
|
+
*/
|
|
776
|
+
const isNarrowedHere = (
|
|
777
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
778
|
+
sourceCode: TSESLint.SourceCode,
|
|
779
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
780
|
+
checker: ts.TypeChecker,
|
|
781
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
782
|
+
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
783
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
784
|
+
node: TSESTree.Node,
|
|
785
|
+
isReceiver: boolean,
|
|
786
|
+
): boolean => {
|
|
787
|
+
if (
|
|
788
|
+
node.type !== AST_NODE_TYPES.Identifier &&
|
|
789
|
+
node.type !== AST_NODE_TYPES.MemberExpression
|
|
790
|
+
) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// A method about to be called (`xs.flat()`): its type is that of the method,
|
|
795
|
+
// which no narrowing of interest changes.
|
|
796
|
+
if (
|
|
797
|
+
node.parent.type === AST_NODE_TYPES.CallExpression &&
|
|
798
|
+
node.parent.callee === node
|
|
799
|
+
) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
804
|
+
if (!isReference(node)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
809
|
+
|
|
810
|
+
if (
|
|
811
|
+
variable?.references.every(
|
|
812
|
+
(reference) => !reference.isWrite() || reference.init === true,
|
|
813
|
+
) === true
|
|
814
|
+
) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const tsNode = getTsNode(node);
|
|
820
|
+
|
|
821
|
+
if (tsNode === undefined) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const declaredType = getDeclaredType(checker, tsNode);
|
|
826
|
+
|
|
827
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
828
|
+
|
|
829
|
+
return (
|
|
830
|
+
declaredType !== undefined &&
|
|
831
|
+
declaredType !== type &&
|
|
832
|
+
// The loop only iterates the receiver itself, so a narrowing that leaves it
|
|
833
|
+
// an array of the same elements (`Arr.isNonEmpty(xs)`) loses nothing.
|
|
834
|
+
!(isReceiver && iteratesTheSame(checker, declaredType, type))
|
|
835
|
+
);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Whether the binding `identifier` reads is declared inside `container`, as the
|
|
840
|
+
* parameter of an arrow function in the receiver is: the loop variable does not
|
|
841
|
+
* shadow it.
|
|
794
842
|
*/
|
|
795
843
|
const isBoundWithin = (
|
|
796
844
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -816,23 +864,8 @@ const isBoundWithin = (
|
|
|
816
864
|
);
|
|
817
865
|
};
|
|
818
866
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
822
|
-
node: TSESTree.Identifier,
|
|
823
|
-
): boolean => {
|
|
824
|
-
const { parent } = node;
|
|
825
|
-
|
|
826
|
-
return !(
|
|
827
|
-
(parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
828
|
-
parent.property === node &&
|
|
829
|
-
!parent.computed) ||
|
|
830
|
-
(parent.type === AST_NODE_TYPES.Property &&
|
|
831
|
-
parent.key === node &&
|
|
832
|
-
!parent.computed &&
|
|
833
|
-
!parent.shorthand)
|
|
834
|
-
);
|
|
835
|
-
};
|
|
867
|
+
const isNode = (value: unknown): value is TSESTree.Node =>
|
|
868
|
+
isRecord(value) && hasKey(value, 'type') && typeof value.type === 'string';
|
|
836
869
|
|
|
837
870
|
/**
|
|
838
871
|
* Whether `node` or a node inside it satisfies `predicate`. A nested
|
|
@@ -877,9 +910,6 @@ const someDescendant = (
|
|
|
877
910
|
});
|
|
878
911
|
};
|
|
879
912
|
|
|
880
|
-
const isNode = (value: unknown): value is TSESTree.Node =>
|
|
881
|
-
isRecord(value) && hasKey(value, 'type') && typeof value.type === 'string';
|
|
882
|
-
|
|
883
913
|
const isReturnStatement = (
|
|
884
914
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
885
915
|
node: TSESTree.Node,
|
|
@@ -894,25 +924,3 @@ const rebindsInGenerator = (
|
|
|
894
924
|
node.type === AST_NODE_TYPES.Super ||
|
|
895
925
|
node.type === AST_NODE_TYPES.MetaProperty ||
|
|
896
926
|
(node.type === AST_NODE_TYPES.Identifier && node.name === 'arguments');
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* Whether `type` is an array or tuple, or a union of them, or an intersection
|
|
900
|
-
* with one in it (`readonly T[] & Brand`).
|
|
901
|
-
*/
|
|
902
|
-
const isArrayOrTupleType = (
|
|
903
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
904
|
-
checker: ts.TypeChecker,
|
|
905
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
906
|
-
type: ts.Type,
|
|
907
|
-
): boolean => {
|
|
908
|
-
const resolved =
|
|
909
|
-
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
910
|
-
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
911
|
-
: type;
|
|
912
|
-
|
|
913
|
-
return resolved.isUnion()
|
|
914
|
-
? resolved.types.every((t) => isArrayOrTupleType(checker, t))
|
|
915
|
-
: resolved.isIntersection()
|
|
916
|
-
? resolved.types.some((t) => isArrayOrTupleType(checker, t))
|
|
917
|
-
: checker.isArrayType(resolved) || checker.isTupleType(resolved);
|
|
918
|
-
};
|