@blumintinc/eslint-plugin-blumint 1.20.136 → 1.20.138
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/enforce-centralized-mock-firestore.js +46 -3
- package/lib/rules/enforce-firestore-set-merge.js +278 -66
- package/lib/rules/enforce-microdiff.js +87 -20
- package/lib/rules/fast-deep-equal-over-microdiff.js +220 -71
- package/lib/rules/key-only-outermost-element.js +159 -21
- package/lib/rules/logical-top-to-bottom-grouping.js +104 -33
- package/lib/rules/no-empty-dependency-use-callbacks.js +116 -43
- package/lib/rules/no-explicit-return-type.js +164 -21
- package/lib/rules/no-mock-firebase-admin.js +52 -8
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +2 -1
- package/lib/rules/no-redundant-usecallback-wrapper.js +126 -70
- package/lib/rules/no-unused-props.js +69 -57
- package/lib/rules/no-useless-usememo-primitives.d.ts +2 -1
- package/lib/rules/no-useless-usememo-primitives.js +156 -47
- package/lib/rules/no-usememo-for-pass-by-value.js +165 -220
- package/lib/rules/prefer-params-over-parent-id.d.ts +2 -1
- package/lib/rules/prefer-params-over-parent-id.js +122 -39
- package/lib/rules/use-latest-callback.js +109 -11
- package/lib/rules/vertically-group-related-functions.js +41 -10
- package/lib/utils/importRemoval.d.ts +88 -0
- package/lib/utils/importRemoval.js +127 -46
- package/lib/utils/patternBindingRemoval.d.ts +28 -0
- package/lib/utils/patternBindingRemoval.js +199 -0
- package/lib/utils/typeDeclarationRemoval.d.ts +32 -0
- package/lib/utils/typeDeclarationRemoval.js +105 -0
- package/package.json +1 -1
- package/release-manifest.json +140 -0
|
@@ -417,10 +417,12 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
417
417
|
DIFF_FUNCTION_NAMES.has(specifier.local.name));
|
|
418
418
|
}
|
|
419
419
|
/**
|
|
420
|
-
*
|
|
420
|
+
* How many references retiring `declaration` hands to the call fixes, or
|
|
421
|
+
* null when retiring it would strand one.
|
|
422
|
+
*
|
|
421
423
|
* The fix removes the whole declaration, so each reference it serves has to
|
|
422
424
|
* be one the call fixes rewrite to `diff` in the same pass — an import swap
|
|
423
|
-
* that strands a reference behind is the defect
|
|
425
|
+
* that strands a reference behind is the defect the null answer guards.
|
|
424
426
|
*
|
|
425
427
|
* Two things have to hold at every reference. `diff` must reach microdiff
|
|
426
428
|
* there: the import rewrite lands at module scope while the references it
|
|
@@ -428,28 +430,71 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
428
430
|
* would resolve to the shadow. And the reference has to sit where a rewrite
|
|
429
431
|
* exists at all, which is the callee position of a convertible call.
|
|
430
432
|
*
|
|
431
|
-
*
|
|
432
|
-
* import
|
|
433
|
+
* The count answers the mirror-image question. A specifier nothing
|
|
434
|
+
* references strands nothing, but it hands the import that replaces it no
|
|
435
|
+
* call either, so a declaration serving zero rewrites is exactly the case
|
|
436
|
+
* where writing `import diff from '@blumintinc/microdiff'` over it binds a
|
|
437
|
+
* name no code reads.
|
|
433
438
|
*/
|
|
434
|
-
function
|
|
439
|
+
function rewrittenReferenceCount(declaration) {
|
|
435
440
|
const claimable = collectClaimableSpecifiers(sourceCode.ast);
|
|
436
441
|
const declarationScope = ASTHelpers_1.ASTHelpers.getScope(context, declaration);
|
|
437
|
-
|
|
442
|
+
let rewrites = 0;
|
|
443
|
+
for (const specifier of declaration.specifiers) {
|
|
438
444
|
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(declarationScope, specifier.local.name);
|
|
439
445
|
if (!variable) {
|
|
440
|
-
|
|
446
|
+
continue;
|
|
441
447
|
}
|
|
442
448
|
const rewritten = isRewrittenSpecifier(specifier);
|
|
443
|
-
|
|
444
|
-
rewritten
|
|
445
|
-
|
|
446
|
-
|
|
449
|
+
for (const reference of variable.references) {
|
|
450
|
+
if (!rewritten ||
|
|
451
|
+
!canEmitDiff(reference.from, claimable) ||
|
|
452
|
+
!isRewrittenCallee(reference.identifier)) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
rewrites += 1;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return rewrites;
|
|
447
459
|
}
|
|
448
460
|
/**
|
|
449
|
-
* Whether the import fix retires `declaration` in this same pass.
|
|
461
|
+
* Whether the import fix retires `declaration` in this same pass. The
|
|
462
|
+
* branches mirror the fix's own: a declaration serving no call rewrite is
|
|
463
|
+
* dropped when the file already imports microdiff, and left alone when
|
|
464
|
+
* dropping it would mean writing an unread `diff` in its place.
|
|
450
465
|
*/
|
|
451
466
|
function willRetireImport(declaration) {
|
|
452
|
-
|
|
467
|
+
if (!canEmitDiffAt(declaration)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const rewrites = rewrittenReferenceCount(declaration);
|
|
471
|
+
if (rewrites === null) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return rewrites > 0 || !!findMicrodiffImport(sourceCode.ast);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Whether a callee's binding keeps a reference this pass leaves alone.
|
|
478
|
+
*
|
|
479
|
+
* Renaming a call to `diff` stops it referencing whatever bound the callee.
|
|
480
|
+
* When the import binding it is retired in the same pass, the binding goes
|
|
481
|
+
* with the declaration and nothing is left behind. A declaration that
|
|
482
|
+
* survives — because a sibling specifier has no rewrite of its own — keeps a
|
|
483
|
+
* name that nothing reads unless some other reference outlives the pass, and
|
|
484
|
+
* an import bound to nothing is what the consumer's `no-unused-vars` and
|
|
485
|
+
* `noUnusedLocals` both fail the build on.
|
|
486
|
+
*
|
|
487
|
+
* A reference the call fixes cannot rewrite is what keeps the binding alive:
|
|
488
|
+
* one in a position that has no rewrite, or one standing where the emitted
|
|
489
|
+
* `diff` would resolve to something else.
|
|
490
|
+
*/
|
|
491
|
+
function keepsUnrewrittenReference(variable) {
|
|
492
|
+
if (!variable) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
const claimable = collectClaimableSpecifiers(sourceCode.ast);
|
|
496
|
+
return variable.references.some((reference) => !isRewrittenCallee(reference.identifier) ||
|
|
497
|
+
!canEmitDiff(reference.from, claimable));
|
|
453
498
|
}
|
|
454
499
|
/**
|
|
455
500
|
* Whether the call at `node`, whose callee `declaration` binds, may be
|
|
@@ -461,15 +506,22 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
461
506
|
* file already has, or the one that replaces `declaration` in this same
|
|
462
507
|
* pass. Renaming a callee whose import survives strands the rewritten call
|
|
463
508
|
* exactly as retiring an import whose callee survives strands that one.
|
|
509
|
+
*
|
|
510
|
+
* It also needs the binding it stops referencing to be accounted for. An
|
|
511
|
+
* existing microdiff import binds the emitted `diff` whatever happens to
|
|
512
|
+
* `declaration`, which is what lets a rename land beside an import too
|
|
513
|
+
* entangled to retire — and that is precisely when the renamed call can have
|
|
514
|
+
* been the last thing reading the name the surviving import binds.
|
|
464
515
|
*/
|
|
465
|
-
function canRewriteCall(node, declaration) {
|
|
516
|
+
function canRewriteCall(node, declaration, calleeVariable) {
|
|
466
517
|
if (!canEmitDiffAt(node) || !hasRewritableArity(node)) {
|
|
467
518
|
return false;
|
|
468
519
|
}
|
|
469
|
-
|
|
470
|
-
|
|
520
|
+
const retires = !!declaration && willRetireImport(declaration);
|
|
521
|
+
if (!retires && !keepsUnrewrittenReference(calleeVariable)) {
|
|
522
|
+
return false;
|
|
471
523
|
}
|
|
472
|
-
return
|
|
524
|
+
return retires || !!findMicrodiffImport(sourceCode.ast);
|
|
473
525
|
}
|
|
474
526
|
/**
|
|
475
527
|
* The fix element that puts microdiff's import at the top of the file, or
|
|
@@ -628,13 +680,28 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
628
680
|
// already binds to something else, and rather than retire an
|
|
629
681
|
// import whose references no call fix rewrites. The report stands
|
|
630
682
|
// either way, so the author resolves it deliberately.
|
|
631
|
-
if (!canEmitDiffAt(node)
|
|
683
|
+
if (!canEmitDiffAt(node)) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
const rewrites = rewrittenReferenceCount(node);
|
|
687
|
+
if (rewrites === null) {
|
|
632
688
|
return null;
|
|
633
689
|
}
|
|
634
690
|
// If we already have a microdiff import, just remove this import
|
|
635
691
|
if (findMicrodiffImport(sourceCode.ast)) {
|
|
636
692
|
return fixer.remove(node);
|
|
637
693
|
}
|
|
694
|
+
// A declaration whose names no call fix rewrites has nothing to
|
|
695
|
+
// hand the import that would replace it, so writing microdiff's
|
|
696
|
+
// import over it trades one unread name for another: the file
|
|
697
|
+
// comes out of the fix with `diff` bound to nothing, which the
|
|
698
|
+
// consumer's `no-unused-vars` and `noUnusedLocals` both fail on.
|
|
699
|
+
// A local shadowing the import is exactly that shape — the calls
|
|
700
|
+
// belong to the shadow, so the import's own reference list is
|
|
701
|
+
// empty — and so is an import nothing uses at all.
|
|
702
|
+
if (rewrites === 0) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
638
705
|
// Otherwise, replace with microdiff import
|
|
639
706
|
return fixer.replaceText(node, MICRODIFF_IMPORT);
|
|
640
707
|
},
|
|
@@ -691,7 +758,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
691
758
|
node,
|
|
692
759
|
messageId: 'enforceMicrodiff',
|
|
693
760
|
fix(fixer) {
|
|
694
|
-
if (!canRewriteCall(node, competingImport)) {
|
|
761
|
+
if (!canRewriteCall(node, competingImport, calleeVariable)) {
|
|
695
762
|
return null;
|
|
696
763
|
}
|
|
697
764
|
return fixer.replaceText(callee, DIFF_NAME);
|
|
@@ -718,7 +785,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
718
785
|
node,
|
|
719
786
|
messageId: 'enforceMicrodiff',
|
|
720
787
|
fix(fixer) {
|
|
721
|
-
if (!canRewriteCall(node, competingImport)) {
|
|
788
|
+
if (!canRewriteCall(node, competingImport, calleeVariable)) {
|
|
722
789
|
return null;
|
|
723
790
|
}
|
|
724
791
|
// When handling fast-diff and similar libraries, need to ensure the function name is replaced
|
|
@@ -6,6 +6,7 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
8
|
const microdiffModules_1 = require("../utils/microdiffModules");
|
|
9
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
9
10
|
const fastDeepEqualModules_1 = require("../utils/fastDeepEqualModules");
|
|
10
11
|
const DIFF_EXPORT_NAME = 'diff';
|
|
11
12
|
/**
|
|
@@ -86,6 +87,14 @@ function unwrapChain(node) {
|
|
|
86
87
|
function rangesOverlap(a, b) {
|
|
87
88
|
return a[0] < b[1] && b[0] < a[1];
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether the diff call sits inside the comparison being rewritten.
|
|
92
|
+
* `diff(a, b).length === 0` rewrites the call where it stands, while
|
|
93
|
+
* `changes.length === 0` compares a call declared elsewhere that the fix moves.
|
|
94
|
+
*/
|
|
95
|
+
function isCallInPlace(node, diffCall) {
|
|
96
|
+
return (diffCall.range[0] >= node.range[0] && diffCall.range[1] <= node.range[1]);
|
|
97
|
+
}
|
|
89
98
|
exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
90
99
|
name: 'fast-deep-equal-over-microdiff',
|
|
91
100
|
meta: {
|
|
@@ -110,12 +119,22 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
110
119
|
let microdiffImportName = 'diff';
|
|
111
120
|
let fastDeepEqualImportName = 'isEqual';
|
|
112
121
|
const reportedNodes = new Set();
|
|
113
|
-
let plannedFastDeepEqualImport = false;
|
|
114
122
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
123
|
+
* Every comparison the rule reports, in traversal order.
|
|
124
|
+
*
|
|
125
|
+
* Reporting is deferred to `Program:exit` because the microdiff import is
|
|
126
|
+
* unbound only once no rewrite leaves a reference to it standing. Judged one
|
|
127
|
+
* comparison at a time, a file with two of them never sees either as the
|
|
128
|
+
* binding's last use, and the pass that rewrites both resolves every report
|
|
129
|
+
* — so nothing ever revisits the stranded import.
|
|
130
|
+
*/
|
|
131
|
+
const violations = [];
|
|
132
|
+
/**
|
|
133
|
+
* A suppressed report is dropped together with its fix, so its rewrite never
|
|
134
|
+
* happens: counting it toward the batch would unbind an import the surviving
|
|
135
|
+
* text still calls, and would hand the `import ... from
|
|
136
|
+
* '@blumintinc/fast-deep-equal'` statement — which rides on one violation's
|
|
137
|
+
* fix — to a violation ESLint is about to discard.
|
|
119
138
|
*/
|
|
120
139
|
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
121
140
|
function isMicrodiffCallee(callee) {
|
|
@@ -434,66 +453,121 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
434
453
|
/**
|
|
435
454
|
* The edits that turn the comparison itself into a fast-deep-equal call.
|
|
436
455
|
*/
|
|
437
|
-
function
|
|
438
|
-
|
|
439
|
-
// splicing the two ranges that actually change: the callee name and the
|
|
440
|
-
// `.length` comparison tail (plus the `!`/`0 ===` head). Re-emitting the
|
|
441
|
-
// argument list instead destroyed everything between the arguments, and a
|
|
442
|
-
// dropped `eslint-disable` comment silently re-enables the rule it was
|
|
443
|
-
// suppressing.
|
|
444
|
-
const isCallInPlace = diffCall.range[0] >= node.range[0] &&
|
|
445
|
-
diffCall.range[1] <= node.range[1];
|
|
446
|
-
if (!isCallInPlace) {
|
|
456
|
+
function createComparisonEdits(node, diffCall, isEquality) {
|
|
457
|
+
if (!isCallInPlace(node, diffCall)) {
|
|
447
458
|
// `changes.length === 0` compares a call declared elsewhere, so that
|
|
448
459
|
// call moves to the comparison's position and its text has to be
|
|
449
460
|
// re-emitted — verbatim, so its comments move with it.
|
|
450
461
|
const call = renameCallee(diffCall);
|
|
451
|
-
return [
|
|
462
|
+
return [{ range: node.range, text: isEquality ? call : `!${call}` }];
|
|
452
463
|
}
|
|
453
|
-
|
|
464
|
+
// Comparing a call in place — `diff(a, b).length === 0` — is rewritten by
|
|
465
|
+
// splicing the ranges that actually change: the callee name and the
|
|
466
|
+
// `.length` comparison tail (plus the `!`/`0 ===` head). Re-emitting the
|
|
467
|
+
// argument list instead destroyed everything between the arguments, and a
|
|
468
|
+
// dropped `eslint-disable` comment silently re-enables the rule it was
|
|
469
|
+
// suppressing.
|
|
470
|
+
const edits = [];
|
|
454
471
|
const headRange = [node.range[0], diffCall.range[0]];
|
|
455
472
|
if (!isEquality) {
|
|
456
|
-
|
|
473
|
+
edits.push({ range: headRange, text: '!' });
|
|
457
474
|
}
|
|
458
475
|
else if (headRange[0] !== headRange[1]) {
|
|
459
|
-
|
|
476
|
+
edits.push({ range: headRange, text: '' });
|
|
477
|
+
}
|
|
478
|
+
edits.push({
|
|
479
|
+
range: diffCall.callee.range,
|
|
480
|
+
text: fastDeepEqualImportName,
|
|
481
|
+
});
|
|
482
|
+
edits.push({ range: [diffCall.range[1], node.range[1]], text: '' });
|
|
483
|
+
return edits;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The positions a rewrite genuinely erases, which is all the orphan
|
|
487
|
+
* analysis may be told about.
|
|
488
|
+
*
|
|
489
|
+
* Only deleted text counts, and the argument list is never deleted: it is
|
|
490
|
+
* spliced around where it stands, or moved verbatim to the comparison's
|
|
491
|
+
* position. Handing over the enclosing span instead would read every binding
|
|
492
|
+
* the arguments mention as unreferenced and delete its import — an
|
|
493
|
+
* over-removal strictly worse than the stranded import this exists to
|
|
494
|
+
* prevent. So each shape gives up the span around the call, plus the callee
|
|
495
|
+
* the rewrite renames.
|
|
496
|
+
*
|
|
497
|
+
* The one case that lists the comparison itself is a hoisted
|
|
498
|
+
* `const changes = diff(...)` this rule will not delete, since then the
|
|
499
|
+
* rewrite really does strip `changes` of its last read. Where the
|
|
500
|
+
* declaration goes too, `changes` goes with it, and listing the comparison
|
|
501
|
+
* would only make the planner refuse a rewrite that orphans nothing.
|
|
502
|
+
*/
|
|
503
|
+
function removedReferenceRanges(node, diffCall, declarationRemoval) {
|
|
504
|
+
if (isCallInPlace(node, diffCall)) {
|
|
505
|
+
return [
|
|
506
|
+
[node.range[0], diffCall.range[0]],
|
|
507
|
+
diffCall.callee.range,
|
|
508
|
+
[diffCall.range[1], node.range[1]],
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
if (!declarationRemoval) {
|
|
512
|
+
return [node.range];
|
|
460
513
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
514
|
+
return [
|
|
515
|
+
[declarationRemoval[0], diffCall.range[0]],
|
|
516
|
+
diffCall.callee.range,
|
|
517
|
+
[diffCall.range[1], declarationRemoval[1]],
|
|
518
|
+
];
|
|
464
519
|
}
|
|
465
520
|
/**
|
|
466
|
-
* The `import ... from '@blumintinc/fast-deep-equal'` edit,
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
521
|
+
* The `import ... from '@blumintinc/fast-deep-equal'` edit, emitted once as
|
|
522
|
+
* part of the file's single fix.
|
|
523
|
+
*
|
|
524
|
+
* The anchor is the microdiff declaration, which the same fix may be
|
|
525
|
+
* deleting. Inserting *after* a declaration that is going away puts the
|
|
526
|
+
* insertion point inside the deleted span, and ESLint rejects that pair as
|
|
527
|
+
* overlapping and discards every message for the file — so a displaced
|
|
528
|
+
* anchor is taken over rather than followed: the new import lands on the
|
|
529
|
+
* exact position the old one occupied.
|
|
471
530
|
*
|
|
472
531
|
* The specifier is the scoped fork: it is the dependency this codebase
|
|
473
532
|
* declares, so any other name would be written as an import that resolves
|
|
474
533
|
* nowhere.
|
|
475
534
|
*/
|
|
476
|
-
function planFastDeepEqualImport(fixer) {
|
|
477
|
-
if (hasFastDeepEqualImport
|
|
535
|
+
function planFastDeepEqualImport(fixer, removalRanges) {
|
|
536
|
+
if (hasFastDeepEqualImport) {
|
|
478
537
|
return [];
|
|
479
538
|
}
|
|
480
|
-
plannedFastDeepEqualImport = true;
|
|
481
539
|
const importStatement = (0, fastDeepEqualModules_1.fastDeepEqualImport)(fastDeepEqualImportName);
|
|
482
540
|
const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
483
|
-
const
|
|
484
|
-
|
|
541
|
+
const removalStartOf = (declaration) => removalRanges.find((range) => range[0] <= declaration.range[0] &&
|
|
542
|
+
range[1] >= declaration.range[1])?.[0];
|
|
543
|
+
const microdiffImports = importDeclarations.filter((declaration) => microdiffModules_1.MICRODIFF_MODULES.has(String(declaration.source.value)));
|
|
544
|
+
for (const declaration of microdiffImports) {
|
|
545
|
+
const start = removalStartOf(declaration);
|
|
546
|
+
if (start !== undefined) {
|
|
547
|
+
return [
|
|
548
|
+
fixer.insertTextBeforeRange([start, start], `${importStatement}\n`),
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const surviving = importDeclarations.filter((declaration) => removalStartOf(declaration) === undefined);
|
|
553
|
+
const anchor = microdiffImports[0] ?? surviving[surviving.length - 1];
|
|
485
554
|
if (anchor) {
|
|
486
555
|
return [fixer.insertTextAfter(anchor, `\n${importStatement}`)];
|
|
487
556
|
}
|
|
488
557
|
return [fixer.insertTextBeforeRange([0, 0], `${importStatement}\n`)];
|
|
489
558
|
}
|
|
490
559
|
/**
|
|
491
|
-
*
|
|
560
|
+
* The edits one violation contributes, or `null` when its rewrite is not
|
|
561
|
+
* provably safe.
|
|
562
|
+
*
|
|
563
|
+
* `removedDeclarations` carries the hoisted declarations earlier violations
|
|
564
|
+
* already claimed, so two comparisons measuring the same
|
|
565
|
+
* `const changes = diff(...)` delete it once and both get rewritten —
|
|
566
|
+
* deleting it twice is an overlap, and deleting it while leaving the second
|
|
567
|
+
* comparison reading `changes` unbinds that read.
|
|
492
568
|
*/
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
// — and leaving the import unscheduled — passes the carrier slot to the
|
|
496
|
-
// first violation that survives.
|
|
569
|
+
function planViolation(violation, removedDeclarations) {
|
|
570
|
+
const { node, diffCall, isEquality, scope } = violation;
|
|
497
571
|
if (isReportSuppressed(node)) {
|
|
498
572
|
return null;
|
|
499
573
|
}
|
|
@@ -501,57 +575,93 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
501
575
|
// makes both halves of the edit wrong: an inserted import collides with a
|
|
502
576
|
// same-scope declaration (TS2440/TS2300), and a narrower-scope shadow
|
|
503
577
|
// rebinds the emitted call to the local value with no diagnostic at all.
|
|
504
|
-
// Resolving from the fixed node's scope chain catches both.
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
const existingBinding = ASTHelpers_1.ASTHelpers.findVariableInScope(
|
|
578
|
+
// Resolving from the fixed node's scope chain catches both. Dropping this
|
|
579
|
+
// violation from the batch leaves the carrier slot to one whose scope is
|
|
580
|
+
// safe, and drops its whole edit — including the removal of a redundant
|
|
581
|
+
// `const changes = diff(...)` — rather than half of it.
|
|
582
|
+
const existingBinding = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, fastDeepEqualImportName);
|
|
509
583
|
if (existingBinding && !bindsFastDeepEqual(existingBinding)) {
|
|
510
584
|
return null;
|
|
511
585
|
}
|
|
512
586
|
if (diffCall.arguments.length !== 2) {
|
|
513
587
|
return null; // Can't fix if not exactly 2 arguments
|
|
514
588
|
}
|
|
515
|
-
const
|
|
589
|
+
const comparisonEdits = createComparisonEdits(node, diffCall, isEquality);
|
|
516
590
|
// If the equality was via an identifier like `changes.length`, and that
|
|
517
591
|
// identifier is declared as `const changes = diff(...);` and used ONLY for
|
|
518
592
|
// `.length` checks, remove the redundant variable declaration.
|
|
519
593
|
const declaration = findRedundantDeclaration(node);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
594
|
+
if (!declaration) {
|
|
595
|
+
return {
|
|
596
|
+
violation,
|
|
597
|
+
edits: comparisonEdits,
|
|
598
|
+
removed: removedReferenceRanges(node, diffCall),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const claimed = removedDeclarations.get(declaration);
|
|
602
|
+
const removalRange = claimed ?? redundantDeclarationRange(declaration);
|
|
603
|
+
// A comparison nested inside the declaration it would delete — a diff
|
|
604
|
+
// argument that reads `changes.length` — admits no pair of disjoint
|
|
605
|
+
// edits. Reporting without a fix beats an overlap, which aborts the
|
|
606
|
+
// file, and beats a half-applied edit that deletes the surviving code.
|
|
607
|
+
if (comparisonEdits.some(({ range }) => rangesOverlap(range, removalRange))) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
violation,
|
|
612
|
+
declaration,
|
|
613
|
+
declarationRemoval: removalRange,
|
|
614
|
+
edits: claimed
|
|
615
|
+
? comparisonEdits
|
|
616
|
+
: [...comparisonEdits, { range: removalRange, text: '' }],
|
|
617
|
+
removed: removedReferenceRanges(node, diffCall, removalRange),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* The rewrites that actually ship, in traversal order.
|
|
622
|
+
*
|
|
623
|
+
* Each is screened alone before it joins the batch: a rewrite whose own
|
|
624
|
+
* deletion orphans something that cannot be unbound safely — a local
|
|
625
|
+
* variable, an import behind a directive comment — would otherwise poison
|
|
626
|
+
* every other rewrite in the file. Edits that collide with an already
|
|
627
|
+
* accepted one are dropped for the same reason a single report's
|
|
628
|
+
* overlapping fixes are: ESLint asserts on the overlap and discards every
|
|
629
|
+
* message for the file.
|
|
630
|
+
*/
|
|
631
|
+
function planViolations() {
|
|
632
|
+
const planned = [];
|
|
633
|
+
const claimedRanges = [];
|
|
634
|
+
const removedDeclarations = new Map();
|
|
635
|
+
for (const violation of violations) {
|
|
636
|
+
const candidate = planViolation(violation, removedDeclarations);
|
|
637
|
+
if (!candidate)
|
|
638
|
+
continue;
|
|
639
|
+
if ((0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, candidate.removed) === null) {
|
|
640
|
+
continue;
|
|
529
641
|
}
|
|
530
|
-
|
|
642
|
+
if (candidate.edits.some(({ range }) => claimedRanges.some((taken) => rangesOverlap(range, taken)))) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
claimedRanges.push(...candidate.edits.map(({ range }) => range));
|
|
646
|
+
if (candidate.declaration && candidate.declarationRemoval) {
|
|
647
|
+
removedDeclarations.set(candidate.declaration, candidate.declarationRemoval);
|
|
648
|
+
}
|
|
649
|
+
planned.push(candidate);
|
|
531
650
|
}
|
|
532
|
-
return
|
|
533
|
-
...planFastDeepEqualImport(fixer),
|
|
534
|
-
...declarationFixes,
|
|
535
|
-
...comparisonFixes,
|
|
536
|
-
];
|
|
651
|
+
return planned;
|
|
537
652
|
}
|
|
538
|
-
function
|
|
653
|
+
function collectEqualityCheck(node, result) {
|
|
539
654
|
const { diffCall, isEquality } = result;
|
|
540
655
|
if (!diffCall)
|
|
541
656
|
return;
|
|
542
657
|
if (reportedNodes.has(node))
|
|
543
658
|
return;
|
|
544
659
|
reportedNodes.add(node);
|
|
545
|
-
|
|
660
|
+
violations.push({
|
|
546
661
|
node,
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
fastEqualName: fastDeepEqualImportName,
|
|
551
|
-
},
|
|
552
|
-
fix(fixer) {
|
|
553
|
-
return createFix(fixer, node, diffCall, isEquality);
|
|
554
|
-
},
|
|
662
|
+
diffCall,
|
|
663
|
+
isEquality,
|
|
664
|
+
scope: ASTHelpers_1.ASTHelpers.getScope(context, node),
|
|
555
665
|
});
|
|
556
666
|
}
|
|
557
667
|
return {
|
|
@@ -599,7 +709,7 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
599
709
|
if (reportedNodes.has(node)) {
|
|
600
710
|
return;
|
|
601
711
|
}
|
|
602
|
-
|
|
712
|
+
collectEqualityCheck(node, isMicrodiffEqualityCheck(node));
|
|
603
713
|
},
|
|
604
714
|
// Check if statements for microdiff equality patterns
|
|
605
715
|
IfStatement(node) {
|
|
@@ -609,7 +719,7 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
609
719
|
if (reportedNodes.has(node.test)) {
|
|
610
720
|
return;
|
|
611
721
|
}
|
|
612
|
-
|
|
722
|
+
collectEqualityCheck(node.test, isMicrodiffEqualityCheck(node.test));
|
|
613
723
|
},
|
|
614
724
|
// Check return statements for microdiff equality patterns
|
|
615
725
|
ReturnStatement(node) {
|
|
@@ -620,7 +730,46 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
620
730
|
if (!argument || reportedNodes.has(argument)) {
|
|
621
731
|
return;
|
|
622
732
|
}
|
|
623
|
-
|
|
733
|
+
collectEqualityCheck(argument, isMicrodiffEqualityCheck(argument));
|
|
734
|
+
},
|
|
735
|
+
'Program:exit'() {
|
|
736
|
+
if (violations.length === 0)
|
|
737
|
+
return;
|
|
738
|
+
const planned = planViolations();
|
|
739
|
+
// One plan over every surviving rewrite: the microdiff binding is left
|
|
740
|
+
// unreferenced by their union even when no single rewrite would strip
|
|
741
|
+
// its last use, and the pass that applies them all resolves every
|
|
742
|
+
// report — so this is the only moment the stranded import is visible.
|
|
743
|
+
const importRemoval = planned.length > 0
|
|
744
|
+
? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, planned.flatMap((entry) => entry.removed))
|
|
745
|
+
: null;
|
|
746
|
+
const removalRanges = importRemoval ?? [];
|
|
747
|
+
// The whole batch ships as one fix, so no rewrite can land without the
|
|
748
|
+
// others the import's orphanhood was judged against, and no unbinding
|
|
749
|
+
// can land without the rewrite it was claimed on. The other violations
|
|
750
|
+
// report without a fixer; the carrier's pass already resolves them.
|
|
751
|
+
//
|
|
752
|
+
// No plan at all means some binding would be left unreferenced yet
|
|
753
|
+
// cannot be unbound safely, so every rewrite stays: reports without a
|
|
754
|
+
// fixer are the lesser damage.
|
|
755
|
+
const carrier = importRemoval ? planned[0] : undefined;
|
|
756
|
+
for (const violation of violations) {
|
|
757
|
+
context.report({
|
|
758
|
+
node: violation.node,
|
|
759
|
+
messageId: 'useFastDeepEqual',
|
|
760
|
+
data: {
|
|
761
|
+
diffName: microdiffImportName,
|
|
762
|
+
fastEqualName: fastDeepEqualImportName,
|
|
763
|
+
},
|
|
764
|
+
fix: violation === carrier?.violation
|
|
765
|
+
? (fixer) => [
|
|
766
|
+
...planFastDeepEqualImport(fixer, removalRanges),
|
|
767
|
+
...removalRanges.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
768
|
+
...planned.flatMap((entry) => entry.edits.map(({ range, text }) => fixer.replaceTextRange(range, text))),
|
|
769
|
+
]
|
|
770
|
+
: undefined,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
624
773
|
},
|
|
625
774
|
};
|
|
626
775
|
},
|