@blumintinc/eslint-plugin-blumint 1.20.68 → 1.20.70
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/lib/index.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +36 -19
- package/lib/rules/enforce-early-destructuring.js +20 -1
- package/lib/rules/enforce-render-hits-memoization.d.ts +2 -1
- package/lib/rules/enforce-render-hits-memoization.js +149 -93
- package/lib/rules/enforce-transform-memoization.js +27 -1
- package/lib/rules/no-useless-usememo-primitives.js +16 -0
- package/lib/rules/optimize-object-boolean-conditions.js +19 -1
- package/lib/rules/parallelize-async-operations.js +66 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.js +270 -5
- package/lib/rules/prefer-type-over-interface.js +38 -0
- package/lib/rules/use-latest-callback.d.ts +6 -1
- package/lib/rules/use-latest-callback.js +235 -46
- package/package.json +1 -1
- package/release-manifest.json +94 -0
|
@@ -785,6 +785,61 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
785
785
|
return null;
|
|
786
786
|
}
|
|
787
787
|
const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
|
|
788
|
+
const startPos = awaitNodes[0].range[0];
|
|
789
|
+
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
790
|
+
// The replacement text is rebuilt from the awaited expressions alone, so
|
|
791
|
+
// a comment inside the replaced span has no representation in it and
|
|
792
|
+
// would be silently deleted. Deleting an eslint-disable-next-line
|
|
793
|
+
// directive re-enables the suppressed rule on the code that survives
|
|
794
|
+
// inside the Promise.all (#1589). Each span comment is therefore either
|
|
795
|
+
// re-hosted directly above the array element built from the statement it
|
|
796
|
+
// annotates, or the fix is declined so no comment is ever destroyed. The
|
|
797
|
+
// report fires either way.
|
|
798
|
+
const spanComments = sourceCode
|
|
799
|
+
.getAllComments()
|
|
800
|
+
.filter((comment) => comment.range[0] >= startPos && comment.range[1] <= endPos);
|
|
801
|
+
const hostedComments = awaitNodes.map(() => []);
|
|
802
|
+
if (spanComments.length > 0) {
|
|
803
|
+
// Re-hosting maps the comments preceding statement i onto element i,
|
|
804
|
+
// which requires the element list to line up 1:1 with the statement
|
|
805
|
+
// list.
|
|
806
|
+
if (awaitExpressions.length !== awaitNodes.length) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
for (const comment of spanComments) {
|
|
810
|
+
const hostIndex = awaitNodes.findIndex((node, index) => index > 0 &&
|
|
811
|
+
comment.range[0] >= awaitNodes[index - 1].range[1] &&
|
|
812
|
+
comment.range[1] <= node.range[0]);
|
|
813
|
+
if (hostIndex === -1) {
|
|
814
|
+
// The comment sits inside one of the merged statements. A comment
|
|
815
|
+
// within the awaited expression itself travels verbatim with
|
|
816
|
+
// getText; anywhere else (between `await` and its operand, or
|
|
817
|
+
// around a declarator's `=`) has no slot in the rebuilt text.
|
|
818
|
+
const isInsideArgument = awaitExpressions.some((expr) => comment.range[0] >= expr.argument.range[0] &&
|
|
819
|
+
comment.range[1] <= expr.argument.range[1]);
|
|
820
|
+
if (!isInsideArgument) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
// A comment that shares the previous statement's last line is a
|
|
826
|
+
// trailing comment (e.g. an eslint-disable-line directive) governing
|
|
827
|
+
// THAT line; moving it above the next element would change which
|
|
828
|
+
// line it applies to.
|
|
829
|
+
if (comment.loc.start.line <= awaitNodes[hostIndex - 1].loc.end.line) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
// A directive above `const x = await f();` may target the
|
|
833
|
+
// declaration's identifier, which the rewrite moves into the
|
|
834
|
+
// destructuring pattern on the Promise.all line — away from every
|
|
835
|
+
// line the re-hosted directive could govern.
|
|
836
|
+
if (/^\s*eslint-/u.test(comment.value) &&
|
|
837
|
+
awaitNodes[hostIndex].type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
hostedComments[hostIndex].push(comment);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
788
843
|
const idsText = [];
|
|
789
844
|
const declKinds = new Set();
|
|
790
845
|
let hasVariableDeclarations = false;
|
|
@@ -817,7 +872,17 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
817
872
|
// the contents of a template literal, where whitespace is significant
|
|
818
873
|
// data rather than formatting and re-indenting would silently change the
|
|
819
874
|
// produced string.
|
|
820
|
-
const elementsText = awaitArguments
|
|
875
|
+
const elementsText = awaitArguments
|
|
876
|
+
.map((argumentText, index) => {
|
|
877
|
+
// Each re-hosted comment lands on its own line directly above the
|
|
878
|
+
// element, which is the only placement where a disable-next-line
|
|
879
|
+
// directive keeps suppressing it.
|
|
880
|
+
const leadingText = (hostedComments[index] ?? [])
|
|
881
|
+
.map((comment) => `${sourceCode.getText(comment)}\n${elementIndent}`)
|
|
882
|
+
.join('');
|
|
883
|
+
return `${leadingText}${argumentText}`;
|
|
884
|
+
})
|
|
885
|
+
.join(`,\n${elementIndent}`);
|
|
821
886
|
let promiseAllText;
|
|
822
887
|
if (hasVariableDeclarations) {
|
|
823
888
|
if (declKinds.size !== 1) {
|
|
@@ -834,8 +899,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
834
899
|
// Simple Promise.all without variable assignments
|
|
835
900
|
promiseAllText = `await Promise.all([\n${elementIndent}${elementsText}\n${baseIndent}]);`;
|
|
836
901
|
}
|
|
837
|
-
const startPos = awaitNodes[0].range[0];
|
|
838
|
-
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
839
902
|
return fixer.replaceTextRange([startPos, endPos], promiseAllText);
|
|
840
903
|
}
|
|
841
904
|
/**
|
|
@@ -62,6 +62,37 @@ function parenthesizeForUnion(text) {
|
|
|
62
62
|
const needsParens = text.includes('=>') || /^new\b/.test(text) || /\bextends\b/.test(text);
|
|
63
63
|
return needsParens ? `(${text})` : text;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* How position-sensitive a comment is when the fix relocates it onto the
|
|
67
|
+
* generated Record:
|
|
68
|
+
*
|
|
69
|
+
* - `next-line`: suppresses exactly the following line
|
|
70
|
+
* (`eslint-disable-next-line`, `@ts-expect-error`, `@ts-ignore`) — hosting
|
|
71
|
+
* it anywhere but directly above the line it suppressed silently changes
|
|
72
|
+
* which rules report.
|
|
73
|
+
* - `own-line`: suppresses its own line (`eslint-disable-line`) — must stay on
|
|
74
|
+
* the same line as the value it annotates.
|
|
75
|
+
* - `range`: opens/closes a suppression region or configures the linter
|
|
76
|
+
* (`eslint-disable`, `eslint-enable`, `eslint-env`, `eslint`, `global`,
|
|
77
|
+
* `exported`) — ESLint honors these only in block-comment form, and
|
|
78
|
+
* relocation can move the region boundary across reported lines, so the fix
|
|
79
|
+
* is never attempted around one.
|
|
80
|
+
* - `none`: prose; content preservation suffices.
|
|
81
|
+
*/
|
|
82
|
+
function directiveKindOf(comment) {
|
|
83
|
+
const text = comment.value.trim();
|
|
84
|
+
if (/^(eslint-disable-next-line|@ts-expect-error|@ts-ignore)\b/.test(text)) {
|
|
85
|
+
return 'next-line';
|
|
86
|
+
}
|
|
87
|
+
if (/^eslint-disable-line\b/.test(text)) {
|
|
88
|
+
return 'own-line';
|
|
89
|
+
}
|
|
90
|
+
if (comment.type === utils_1.AST_TOKEN_TYPES.Block &&
|
|
91
|
+
/^(eslint-disable|eslint-enable|eslint-env|eslint\b|globals?\b|exported\b)/.test(text)) {
|
|
92
|
+
return 'range';
|
|
93
|
+
}
|
|
94
|
+
return 'none';
|
|
95
|
+
}
|
|
65
96
|
exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
66
97
|
name: 'prefer-map-over-conditional-dispatch',
|
|
67
98
|
meta: {
|
|
@@ -360,7 +391,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
360
391
|
}
|
|
361
392
|
const stmt = stmts[0];
|
|
362
393
|
if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
|
|
363
|
-
return { kind: 'return', expr: stmt.argument };
|
|
394
|
+
return { kind: 'return', expr: stmt.argument, stmt };
|
|
364
395
|
}
|
|
365
396
|
if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
|
|
366
397
|
stmt.expression.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
@@ -369,6 +400,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
369
400
|
kind: 'assign',
|
|
370
401
|
target: stmt.expression.left,
|
|
371
402
|
expr: stmt.expression.right,
|
|
403
|
+
stmt,
|
|
372
404
|
};
|
|
373
405
|
}
|
|
374
406
|
return null;
|
|
@@ -504,13 +536,92 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
504
536
|
return match ? match[0] : '';
|
|
505
537
|
}
|
|
506
538
|
function buildRecordText(name, dText, vText, entries, baseIndent) {
|
|
507
|
-
const lines = entries.
|
|
539
|
+
const lines = entries.flatMap((e) => {
|
|
540
|
+
const hosted = (e.leadingComments ?? []).map((comment) => `${baseIndent} ${comment}`);
|
|
541
|
+
const trailing = e.trailingComments && e.trailingComments.length > 0
|
|
542
|
+
? ` ${e.trailingComments.join(' ')}`
|
|
543
|
+
: '';
|
|
544
|
+
hosted.push(`${baseIndent} ${formatKey(e.key)}: ${e.valueText},${trailing}`);
|
|
545
|
+
return hosted;
|
|
546
|
+
});
|
|
508
547
|
return [
|
|
509
548
|
`const ${name}: Record<${dText}, ${vText}> = {`,
|
|
510
549
|
...lines,
|
|
511
550
|
`${baseIndent}};`,
|
|
512
551
|
].join('\n');
|
|
513
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Decides where every comment inside the replaced construct lands on the
|
|
555
|
+
* generated Record, mutating `entries` with the hosted text. Comments
|
|
556
|
+
* inside a carried span (a branch value expression, the discriminant, the
|
|
557
|
+
* assignment target) survive verbatim inside the copied text; comments
|
|
558
|
+
* inside a dropped span (an unreachable default/tail) die together with
|
|
559
|
+
* the code they annotate, which is not an orphaning. Every other comment
|
|
560
|
+
* must be hosted onto a map entry — leading comments go on the line(s)
|
|
561
|
+
* directly above the entry, same-line trailing comments append after it.
|
|
562
|
+
* Returns false when a comment cannot be hosted without changing what it
|
|
563
|
+
* annotates or suppresses; the caller then withholds the autofix so a
|
|
564
|
+
* directive is never silently deleted or retargeted.
|
|
565
|
+
*/
|
|
566
|
+
function planCommentHosting(args) {
|
|
567
|
+
const { container, carriedSpans, droppedSpans, anchors, entries } = args;
|
|
568
|
+
const ordered = [...anchors].sort((a, b) => a.range[0] - b.range[0]);
|
|
569
|
+
const hostLeading = (entry, text) => {
|
|
570
|
+
entry.leadingComments = [...(entry.leadingComments ?? []), text];
|
|
571
|
+
};
|
|
572
|
+
const hostTrailing = (entry, text) => {
|
|
573
|
+
entry.trailingComments = [...(entry.trailingComments ?? []), text];
|
|
574
|
+
};
|
|
575
|
+
for (const comment of sourceCode.getCommentsInside(container)) {
|
|
576
|
+
const [cStart, cEnd] = comment.range;
|
|
577
|
+
const within = (spans) => spans.some(([start, end]) => cStart >= start && cEnd <= end);
|
|
578
|
+
if (within(carriedSpans) || within(droppedSpans)) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const kind = directiveKindOf(comment);
|
|
582
|
+
if (kind === 'range') {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const text = sourceCode.getText(comment);
|
|
586
|
+
// Same-line trailing (`doThing(); // note`): append to the entry line
|
|
587
|
+
// so an `eslint-disable-line` keeps suppressing the line its value
|
|
588
|
+
// lands on. The last matching anchor wins so a comment trailing two
|
|
589
|
+
// same-line statements attaches to the nearer one.
|
|
590
|
+
const trailingCandidates = ordered.filter((anchor) => anchor.endLine === comment.loc.start.line &&
|
|
591
|
+
cStart >= anchor.range[1]);
|
|
592
|
+
const trailingAnchor = trailingCandidates[trailingCandidates.length - 1];
|
|
593
|
+
if (trailingAnchor) {
|
|
594
|
+
if (kind === 'next-line') {
|
|
595
|
+
// A trailing disable-next-line targets whatever follows the
|
|
596
|
+
// statement; the generated map has no equivalent following line.
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
if (kind === 'own-line' && trailingAnchor.entryCount > 1) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
hostTrailing(entries[trailingAnchor.entryIndex], text);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// A comment inside the value statement but outside the copied
|
|
606
|
+
// expression (`return /* why */ value;`) belongs to that branch.
|
|
607
|
+
const containingAnchor = ordered.find((anchor) => cStart >= anchor.range[0] && cEnd <= anchor.range[1]);
|
|
608
|
+
const leadingAnchor = containingAnchor ?? ordered.find((anchor) => anchor.range[0] >= cEnd);
|
|
609
|
+
if (!leadingAnchor) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (kind === 'next-line') {
|
|
613
|
+
// Host a line-targeted directive only when it provably targeted the
|
|
614
|
+
// branch's value line, so relocation preserves — never widens or
|
|
615
|
+
// drops — the suppression.
|
|
616
|
+
if (leadingAnchor.entryCount > 1 ||
|
|
617
|
+
leadingAnchor.startLine !== comment.loc.end.line + 1) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
hostLeading(entries[leadingAnchor.entryIndex], text);
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
514
625
|
/**
|
|
515
626
|
* Given ordered explicit branches + optional tail, resolve coverage against
|
|
516
627
|
* the union's literal keys and (when full) the ordered Record entries.
|
|
@@ -564,20 +675,28 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
564
675
|
if (!flags.canPlaceFix) {
|
|
565
676
|
return 'the dispatch sits inside an expression-bodied function; extract the Record manually so it stays in scope';
|
|
566
677
|
}
|
|
678
|
+
if (!flags.commentSafe) {
|
|
679
|
+
return 'a comment inside the dispatch cannot be carried onto the generated Record without changing what it annotates or suppresses — relocate the comment, then convert';
|
|
680
|
+
}
|
|
567
681
|
return 'a collision-free lookup name could not be derived from the discriminant';
|
|
568
682
|
}
|
|
569
683
|
function report(node, analysis) {
|
|
570
|
-
const { entries, contributingValues, dText, form, assignTargetText, fullCoverage, hasNullish, canPlaceFix, } = analysis;
|
|
684
|
+
const { entries, contributingValues, dText, form, assignTargetText, fullCoverage, hasNullish, canPlaceFix, commentBlocked, } = analysis;
|
|
571
685
|
const eagerSafe = contributingValues.every((expr) => !containsEagerUnsafe(expr));
|
|
572
686
|
let name = null;
|
|
573
687
|
// Name derivation is only needed for the autofix path.
|
|
574
|
-
if (fullCoverage &&
|
|
688
|
+
if (fullCoverage &&
|
|
689
|
+
!hasNullish &&
|
|
690
|
+
eagerSafe &&
|
|
691
|
+
canPlaceFix &&
|
|
692
|
+
!commentBlocked) {
|
|
575
693
|
name = deriveLookupName(discriminantOf(node));
|
|
576
694
|
}
|
|
577
695
|
const autofixable = fullCoverage &&
|
|
578
696
|
!hasNullish &&
|
|
579
697
|
eagerSafe &&
|
|
580
698
|
canPlaceFix &&
|
|
699
|
+
!commentBlocked &&
|
|
581
700
|
name !== null;
|
|
582
701
|
if (!autofixable) {
|
|
583
702
|
context.report({
|
|
@@ -589,6 +708,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
589
708
|
hasNullish,
|
|
590
709
|
eagerSafe,
|
|
591
710
|
canPlaceFix,
|
|
711
|
+
commentSafe: !commentBlocked,
|
|
592
712
|
}),
|
|
593
713
|
},
|
|
594
714
|
});
|
|
@@ -700,7 +820,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
700
820
|
continue;
|
|
701
821
|
}
|
|
702
822
|
const value = extractBranchValue(c.consequent);
|
|
703
|
-
parsed.push({ tests: [...pending, c.test], value });
|
|
823
|
+
parsed.push({ tests: [...pending, c.test], value, caseNode: c });
|
|
704
824
|
pending = [];
|
|
705
825
|
}
|
|
706
826
|
if (pending.length > 0) {
|
|
@@ -714,6 +834,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
714
834
|
const explicit = [];
|
|
715
835
|
let defaultValue;
|
|
716
836
|
let hasDefault = false;
|
|
837
|
+
let defaultCaseNode = null;
|
|
717
838
|
for (const branch of parsed) {
|
|
718
839
|
const literalTests = branch.tests.filter((t) => t !== null);
|
|
719
840
|
const isDefaultGroup = literalTests.length !== branch.tests.length;
|
|
@@ -724,6 +845,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
724
845
|
}
|
|
725
846
|
hasDefault = true;
|
|
726
847
|
defaultValue = branch.value;
|
|
848
|
+
defaultCaseNode = branch.caseNode;
|
|
727
849
|
continue;
|
|
728
850
|
}
|
|
729
851
|
if (!branch.value) {
|
|
@@ -798,6 +920,57 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
798
920
|
if (isNarrowingExempt(node.discriminant, contributingValues)) {
|
|
799
921
|
return;
|
|
800
922
|
}
|
|
923
|
+
// Comment hosting only gates the autofix path (partial coverage is
|
|
924
|
+
// already report-only), so it is planned for full coverage alone.
|
|
925
|
+
let commentBlocked = false;
|
|
926
|
+
if (coverage.fullCoverage) {
|
|
927
|
+
const tailUsed = coverage.remainingCount === 1 && defaultVal !== null;
|
|
928
|
+
const carriedSpans = [node.discriminant.range];
|
|
929
|
+
const droppedSpans = [];
|
|
930
|
+
const anchors = [];
|
|
931
|
+
let entryIndex = 0;
|
|
932
|
+
for (const branch of explicit) {
|
|
933
|
+
carriedSpans.push(branch.value.expr.range);
|
|
934
|
+
anchors.push({
|
|
935
|
+
range: branch.value.stmt.range,
|
|
936
|
+
startLine: branch.value.stmt.loc.start.line,
|
|
937
|
+
endLine: branch.value.stmt.loc.end.line,
|
|
938
|
+
entryIndex,
|
|
939
|
+
entryCount: branch.keys.length,
|
|
940
|
+
});
|
|
941
|
+
entryIndex += branch.keys.length;
|
|
942
|
+
}
|
|
943
|
+
if (kind === 'assign' && explicit[0].value.kind === 'assign') {
|
|
944
|
+
carriedSpans.push(explicit[0].value.target.range);
|
|
945
|
+
}
|
|
946
|
+
if (tailUsed && defaultVal) {
|
|
947
|
+
carriedSpans.push(defaultVal.expr.range);
|
|
948
|
+
anchors.push({
|
|
949
|
+
range: defaultVal.stmt.range,
|
|
950
|
+
startLine: defaultVal.stmt.loc.start.line,
|
|
951
|
+
endLine: defaultVal.stmt.loc.end.line,
|
|
952
|
+
entryIndex: coverage.entries.length - 1,
|
|
953
|
+
entryCount: 1,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
else if (defaultCaseNode) {
|
|
957
|
+
// The unreachable default is deleted wholesale; its comments —
|
|
958
|
+
// including any directly above the `default:` label — annotate
|
|
959
|
+
// deleted code and die with it.
|
|
960
|
+
const caseIndex = node.cases.indexOf(defaultCaseNode);
|
|
961
|
+
const droppedStart = caseIndex > 0
|
|
962
|
+
? node.cases[caseIndex - 1].range[1]
|
|
963
|
+
: node.discriminant.range[1];
|
|
964
|
+
droppedSpans.push([droppedStart, defaultCaseNode.range[1]]);
|
|
965
|
+
}
|
|
966
|
+
commentBlocked = !planCommentHosting({
|
|
967
|
+
container: node,
|
|
968
|
+
carriedSpans,
|
|
969
|
+
droppedSpans,
|
|
970
|
+
anchors,
|
|
971
|
+
entries: coverage.entries,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
801
974
|
report(node, {
|
|
802
975
|
entries: coverage.entries,
|
|
803
976
|
contributingValues,
|
|
@@ -807,6 +980,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
807
980
|
fullCoverage: coverage.fullCoverage,
|
|
808
981
|
hasNullish,
|
|
809
982
|
canPlaceFix: true,
|
|
983
|
+
commentBlocked,
|
|
810
984
|
});
|
|
811
985
|
}
|
|
812
986
|
// ---- Ternary form -------------------------------------------------------
|
|
@@ -906,6 +1080,47 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
906
1080
|
crossesFunction = true;
|
|
907
1081
|
}
|
|
908
1082
|
}
|
|
1083
|
+
let commentBlocked = false;
|
|
1084
|
+
if (coverage.fullCoverage) {
|
|
1085
|
+
const tailUsed = coverage.remainingCount === 1;
|
|
1086
|
+
const carriedSpans = [
|
|
1087
|
+
head.discNode.range,
|
|
1088
|
+
...links.map((l) => l.expr.range),
|
|
1089
|
+
];
|
|
1090
|
+
const droppedSpans = [];
|
|
1091
|
+
const anchors = links.map((l, i) => ({
|
|
1092
|
+
range: l.expr.range,
|
|
1093
|
+
startLine: l.expr.loc.start.line,
|
|
1094
|
+
endLine: l.expr.loc.end.line,
|
|
1095
|
+
entryIndex: i,
|
|
1096
|
+
entryCount: 1,
|
|
1097
|
+
}));
|
|
1098
|
+
if (tailUsed) {
|
|
1099
|
+
carriedSpans.push(tailExpr.range);
|
|
1100
|
+
anchors.push({
|
|
1101
|
+
range: tailExpr.range,
|
|
1102
|
+
startLine: tailExpr.loc.start.line,
|
|
1103
|
+
endLine: tailExpr.loc.end.line,
|
|
1104
|
+
entryIndex: coverage.entries.length - 1,
|
|
1105
|
+
entryCount: 1,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
// The unreachable tail is deleted; comments between the last kept
|
|
1110
|
+
// consequent and the tail's end annotate deleted code.
|
|
1111
|
+
droppedSpans.push([
|
|
1112
|
+
links[links.length - 1].expr.range[1],
|
|
1113
|
+
tailExpr.range[1],
|
|
1114
|
+
]);
|
|
1115
|
+
}
|
|
1116
|
+
commentBlocked = !planCommentHosting({
|
|
1117
|
+
container: node,
|
|
1118
|
+
carriedSpans,
|
|
1119
|
+
droppedSpans,
|
|
1120
|
+
anchors,
|
|
1121
|
+
entries: coverage.entries,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
909
1124
|
discriminantMap.set(node, head.discNode);
|
|
910
1125
|
report(node, {
|
|
911
1126
|
entries: coverage.entries,
|
|
@@ -915,6 +1130,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
915
1130
|
fullCoverage: coverage.fullCoverage,
|
|
916
1131
|
hasNullish,
|
|
917
1132
|
canPlaceFix: !crossesFunction,
|
|
1133
|
+
commentBlocked,
|
|
918
1134
|
});
|
|
919
1135
|
}
|
|
920
1136
|
// ---- if / else-if form --------------------------------------------------
|
|
@@ -935,6 +1151,10 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
935
1151
|
}
|
|
936
1152
|
const links = [];
|
|
937
1153
|
let tail = null;
|
|
1154
|
+
// The final else statement/block and the end of the consequent before
|
|
1155
|
+
// it — the source region a dropped tail's comments die with.
|
|
1156
|
+
let tailNode = null;
|
|
1157
|
+
let tailPrevEnd = 0;
|
|
938
1158
|
let cur = node;
|
|
939
1159
|
while (cur) {
|
|
940
1160
|
const link = equalityDiscriminant(cur.test);
|
|
@@ -960,6 +1180,8 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
960
1180
|
return;
|
|
961
1181
|
}
|
|
962
1182
|
tail = tailValue;
|
|
1183
|
+
tailNode = alt;
|
|
1184
|
+
tailPrevEnd = cur.consequent.range[1];
|
|
963
1185
|
cur = null;
|
|
964
1186
|
}
|
|
965
1187
|
if (links.length === 0) {
|
|
@@ -1012,6 +1234,48 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1012
1234
|
if (isNarrowingExempt(head.discNode, contributingValues)) {
|
|
1013
1235
|
return;
|
|
1014
1236
|
}
|
|
1237
|
+
let commentBlocked = false;
|
|
1238
|
+
if (coverage.fullCoverage) {
|
|
1239
|
+
const tailUsed = coverage.remainingCount === 1 && tail !== null;
|
|
1240
|
+
const carriedSpans = [head.discNode.range];
|
|
1241
|
+
const droppedSpans = [];
|
|
1242
|
+
const anchors = [];
|
|
1243
|
+
links.forEach((link, i) => {
|
|
1244
|
+
carriedSpans.push(link.value.expr.range);
|
|
1245
|
+
anchors.push({
|
|
1246
|
+
range: link.value.stmt.range,
|
|
1247
|
+
startLine: link.value.stmt.loc.start.line,
|
|
1248
|
+
endLine: link.value.stmt.loc.end.line,
|
|
1249
|
+
entryIndex: i,
|
|
1250
|
+
entryCount: 1,
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
if (kind === 'assign' && links[0].value.kind === 'assign') {
|
|
1254
|
+
carriedSpans.push(links[0].value.target.range);
|
|
1255
|
+
}
|
|
1256
|
+
if (tailUsed && tail) {
|
|
1257
|
+
carriedSpans.push(tail.expr.range);
|
|
1258
|
+
anchors.push({
|
|
1259
|
+
range: tail.stmt.range,
|
|
1260
|
+
startLine: tail.stmt.loc.start.line,
|
|
1261
|
+
endLine: tail.stmt.loc.end.line,
|
|
1262
|
+
entryIndex: coverage.entries.length - 1,
|
|
1263
|
+
entryCount: 1,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
else if (tailNode) {
|
|
1267
|
+
// The unreachable else is deleted; comments from the last kept
|
|
1268
|
+
// consequent through the else's end annotate deleted code.
|
|
1269
|
+
droppedSpans.push([tailPrevEnd, tailNode.range[1]]);
|
|
1270
|
+
}
|
|
1271
|
+
commentBlocked = !planCommentHosting({
|
|
1272
|
+
container: node,
|
|
1273
|
+
carriedSpans,
|
|
1274
|
+
droppedSpans,
|
|
1275
|
+
anchors,
|
|
1276
|
+
entries: coverage.entries,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1015
1279
|
discriminantMap.set(node, head.discNode);
|
|
1016
1280
|
report(node, {
|
|
1017
1281
|
entries: coverage.entries,
|
|
@@ -1022,6 +1286,7 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
1022
1286
|
fullCoverage: coverage.fullCoverage,
|
|
1023
1287
|
hasNullish,
|
|
1024
1288
|
canPlaceFix: true,
|
|
1289
|
+
commentBlocked,
|
|
1025
1290
|
});
|
|
1026
1291
|
}
|
|
1027
1292
|
return {
|
|
@@ -47,6 +47,38 @@ function isInsideModuleAugmentation(node) {
|
|
|
47
47
|
}
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Declaration merging is what `interface` can do and `type` cannot, so a name
|
|
52
|
+
* carrying more than one declaration is not a stylistic choice: rewriting one
|
|
53
|
+
* of the halves emits two declarations of the same name (TS2300) and silently
|
|
54
|
+
* splits the merged shape, so members that used to coexist no longer do.
|
|
55
|
+
*
|
|
56
|
+
* The check keys on the **declaring scope**, not on a count of the name across
|
|
57
|
+
* the file, because merging is a property of the declaration space. Two
|
|
58
|
+
* same-named interfaces in different function bodies or blocks are distinct
|
|
59
|
+
* types that never merge, and each of those still converts cleanly.
|
|
60
|
+
*
|
|
61
|
+
* Any second definition counts, not only another interface. `class A {}` and
|
|
62
|
+
* `enum A {}` occupy the same type-space slot a type alias would claim, so
|
|
63
|
+
* those conversions break the build too; `function A() {}` and
|
|
64
|
+
* `namespace A {}` are value-space only and would survive the rewrite, but
|
|
65
|
+
* they are rare enough that treating every co-declaration as merged costs
|
|
66
|
+
* almost nothing and keeps the rule from having to model TypeScript's merging
|
|
67
|
+
* table.
|
|
68
|
+
*/
|
|
69
|
+
function isMergedDeclaration(context, node) {
|
|
70
|
+
// The variable is located by the definition that points back at *this*
|
|
71
|
+
// declaration rather than by taking the first entry, so a parser that also
|
|
72
|
+
// surfaces the type parameters here cannot make the count read off the
|
|
73
|
+
// wrong name.
|
|
74
|
+
const declared = context
|
|
75
|
+
.getDeclaredVariables(node)
|
|
76
|
+
.find((variable) => variable.defs.some((definition) => definition.node === node));
|
|
77
|
+
// A scope manager that never registered the name tells us nothing about
|
|
78
|
+
// merging; falling through preserves the report rather than exempting on a
|
|
79
|
+
// missing signal.
|
|
80
|
+
return declared !== undefined && declared.defs.length > 1;
|
|
81
|
+
}
|
|
50
82
|
exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
51
83
|
name: 'prefer-type-over-interface',
|
|
52
84
|
meta: {
|
|
@@ -70,6 +102,12 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
|
70
102
|
if (isInsideModuleAugmentation(node)) {
|
|
71
103
|
return;
|
|
72
104
|
}
|
|
105
|
+
// Reporting without a fix would be unactionable here — the author
|
|
106
|
+
// cannot honour it without collapsing the merge by hand — and an
|
|
107
|
+
// unactionable report is what manufactures `eslint-disable` (#1568).
|
|
108
|
+
if (isMergedDeclaration(context, node)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
73
111
|
context.report({
|
|
74
112
|
node,
|
|
75
113
|
messageId: 'preferType',
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
printWidth?: number;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const useLatestCallback: TSESLint.RuleModule<"useLatestCallback", Options, TSESLint.RuleListener>;
|
|
3
8
|
export default useLatestCallback;
|