@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
|
@@ -7,6 +7,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.preferParamsOverParentId = void 0;
|
|
8
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
11
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
12
|
+
const patternBindingRemoval_1 = require("../utils/patternBindingRemoval");
|
|
13
|
+
function rangesOverlap(a, b) {
|
|
14
|
+
return a[0] < b[1] && b[0] < a[1];
|
|
15
|
+
}
|
|
10
16
|
const HANDLER_TYPES = new Set([
|
|
11
17
|
'DocumentChangeHandler',
|
|
12
18
|
'DocumentChangeHandlerTransaction',
|
|
@@ -220,6 +226,57 @@ exports.preferParamsOverParentId = (0, createRule_1.createRule)({
|
|
|
220
226
|
defaultOptions: [],
|
|
221
227
|
create(context) {
|
|
222
228
|
const handlerNodes = new Set();
|
|
229
|
+
const sourceCode = context.sourceCode;
|
|
230
|
+
/**
|
|
231
|
+
* Every `ref.parent...id` read the rule reports, in traversal order.
|
|
232
|
+
*
|
|
233
|
+
* Reporting waits for `Program:exit` because the binding the read walks from
|
|
234
|
+
* — `change` in `const { data: change, params } = event` — is left
|
|
235
|
+
* unreferenced only once NO surviving read mentions it. Judged one read at a
|
|
236
|
+
* time, a handler with two of them never sees either as the binding's last
|
|
237
|
+
* use, and the pass that rewrites both resolves every report, so nothing
|
|
238
|
+
* revisits the stranded binding.
|
|
239
|
+
*/
|
|
240
|
+
const violations = [];
|
|
241
|
+
/**
|
|
242
|
+
* A suppressed report is discarded together with its fix, so its rewrite
|
|
243
|
+
* never happens: counting it toward the batch would retire a property the
|
|
244
|
+
* surviving text still reads.
|
|
245
|
+
*/
|
|
246
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
247
|
+
/**
|
|
248
|
+
* The bindings a rewrite strands are destructuring properties rather than
|
|
249
|
+
* imports, so the import machinery is handed the pattern planner to unbind
|
|
250
|
+
* them with.
|
|
251
|
+
*/
|
|
252
|
+
const planRemoval = (removed) => (0, importRemoval_1.planOrphanedBindingRemoval)(sourceCode, removed, (variables, ranges) => (0, patternBindingRemoval_1.planPatternBindingRemoval)(sourceCode, variables, ranges));
|
|
253
|
+
/**
|
|
254
|
+
* The rewrites that ship, in traversal order.
|
|
255
|
+
*
|
|
256
|
+
* Each is screened alone before joining the batch: a rewrite whose own
|
|
257
|
+
* deletion strands something that cannot be unbound safely would otherwise
|
|
258
|
+
* withhold every other rewrite in the file. Overlapping edits are dropped
|
|
259
|
+
* because ESLint discards every message for a file whose fixes collide.
|
|
260
|
+
*/
|
|
261
|
+
function planViolations() {
|
|
262
|
+
const planned = [];
|
|
263
|
+
const claimed = [];
|
|
264
|
+
for (const violation of violations) {
|
|
265
|
+
const { replacement } = violation;
|
|
266
|
+
if (!replacement)
|
|
267
|
+
continue;
|
|
268
|
+
if (isReportSuppressed(violation.node))
|
|
269
|
+
continue;
|
|
270
|
+
if (planRemoval([violation.node.range]) === null)
|
|
271
|
+
continue;
|
|
272
|
+
if (claimed.some((taken) => rangesOverlap(violation.node.range, taken))) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
claimed.push(violation.node.range);
|
|
276
|
+
planned.push({ violation, replacement });
|
|
277
|
+
}
|
|
278
|
+
return planned;
|
|
279
|
+
}
|
|
223
280
|
return {
|
|
224
281
|
// Track Firebase change handler functions
|
|
225
282
|
'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
|
|
@@ -230,45 +287,71 @@ exports.preferParamsOverParentId = (0, createRule_1.createRule)({
|
|
|
230
287
|
// Detect .ref.parent.id patterns
|
|
231
288
|
MemberExpression(node) {
|
|
232
289
|
const parentAccess = isParentIdAccess(node);
|
|
233
|
-
if (parentAccess.isMatch) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
if (!parentAccess.isMatch) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const handlerNode = findHandlerFunction(node, handlerNodes);
|
|
294
|
+
if (!handlerNode) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const paramName = getParentParamName(parentAccess.depth);
|
|
298
|
+
const paramsInScope = getParamsInScope(handlerNode);
|
|
299
|
+
if (!paramsInScope) {
|
|
300
|
+
violations.push({ node, paramName, replacement: null });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const hasOptional = hasOptionalChaining(node);
|
|
304
|
+
let replacement = null;
|
|
305
|
+
if (paramsInScope.identifier) {
|
|
306
|
+
replacement = hasOptional
|
|
307
|
+
? `${paramsInScope.identifier}?.${paramName}`
|
|
308
|
+
: `${paramsInScope.identifier}.${paramName}`;
|
|
309
|
+
}
|
|
310
|
+
else if (paramsInScope.properties) {
|
|
311
|
+
replacement = paramsInScope.properties.get(paramName) ?? null;
|
|
312
|
+
}
|
|
313
|
+
violations.push({ node, paramName, replacement });
|
|
314
|
+
},
|
|
315
|
+
'Program:exit'() {
|
|
316
|
+
if (violations.length === 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const planned = planViolations();
|
|
320
|
+
// One plan over every surviving rewrite: a binding read by two of them
|
|
321
|
+
// is unreferenced only under their union, and the pass that applies
|
|
322
|
+
// them all resolves every report — so this is the only moment the
|
|
323
|
+
// stranded binding is visible.
|
|
324
|
+
const removal = planned.length > 0
|
|
325
|
+
? planRemoval(planned.map((entry) => entry.violation.node.range))
|
|
326
|
+
: null;
|
|
327
|
+
// ESLint throws out every message for a file whose fix ranges overlap,
|
|
328
|
+
// so a plan that collides with the rewrites it was computed for is
|
|
329
|
+
// withheld rather than allowed to take the pass down with it.
|
|
330
|
+
const collides = removal?.some((range) => planned.some((entry) => rangesOverlap(entry.violation.node.range, [range[0], range[1]])));
|
|
331
|
+
const removalRanges = removal && !collides ? removal : null;
|
|
332
|
+
// The whole batch ships as one fix, so no rewrite lands without the
|
|
333
|
+
// others its binding's orphanhood was judged against, and no unbinding
|
|
334
|
+
// lands without the rewrites it was claimed on. The remaining
|
|
335
|
+
// violations report without a fixer; the carrier's pass resolves them.
|
|
336
|
+
//
|
|
337
|
+
// No plan at all means some binding would be left unreferenced yet
|
|
338
|
+
// cannot be unbound safely, so every rewrite stays behind: an unfixed
|
|
339
|
+
// report is the lesser damage.
|
|
340
|
+
const carrier = removalRanges ? planned[0] : undefined;
|
|
341
|
+
for (const violation of violations) {
|
|
342
|
+
context.report({
|
|
343
|
+
node: violation.node,
|
|
344
|
+
messageId: 'preferParams',
|
|
345
|
+
data: {
|
|
346
|
+
paramName: violation.paramName,
|
|
347
|
+
},
|
|
348
|
+
fix: violation === carrier?.violation
|
|
349
|
+
? (fixer) => [
|
|
350
|
+
...(removalRanges ?? []).map((range) => fixer.removeRange([range[0], range[1]])),
|
|
351
|
+
...planned.map((entry) => fixer.replaceText(entry.violation.node, entry.replacement)),
|
|
352
|
+
]
|
|
353
|
+
: undefined,
|
|
354
|
+
});
|
|
272
355
|
}
|
|
273
356
|
},
|
|
274
357
|
};
|
|
@@ -4,6 +4,7 @@ exports.useLatestCallback = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
7
8
|
/**
|
|
8
9
|
* Matches Prettier's own default. Dropping the dependency array lets the call
|
|
9
10
|
* collapse onto one line, so the fixer decides a line break a formatter owns: a
|
|
@@ -774,17 +775,40 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
774
775
|
: openBrace.range[0];
|
|
775
776
|
return [fixer.removeRange([start, closeBrace.range[1]])];
|
|
776
777
|
};
|
|
777
|
-
|
|
778
|
+
/**
|
|
779
|
+
* The hook's own import, placed at the react import it displaces.
|
|
780
|
+
*
|
|
781
|
+
* The anchor is nudged to the start of any planned deletion that
|
|
782
|
+
* encloses it, because a fix's edits must not overlap: an unbinding
|
|
783
|
+
* that swallows the react declaration's whole line starts before the
|
|
784
|
+
* declaration whenever that line is indented, and inserting inside
|
|
785
|
+
* that span is the one position ESLint rejects outright.
|
|
786
|
+
*/
|
|
787
|
+
const insertImportFixes = (fixer, unbindings) => {
|
|
788
|
+
if (latestCallbackImport) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
let at = statement.range[0];
|
|
792
|
+
for (const [start, end] of unbindings) {
|
|
793
|
+
if (start < at && at < end) {
|
|
794
|
+
at = start;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return [fixer.insertTextBeforeRange([at, at], `${importText}\n`)];
|
|
798
|
+
};
|
|
799
|
+
const importFixes = (fixer, unbindsReactSpecifiers, unbindings) => {
|
|
778
800
|
if (!touchesImport) {
|
|
779
801
|
return [];
|
|
780
802
|
}
|
|
803
|
+
// The orphan plan already unbinds every react specifier the rewrite
|
|
804
|
+
// strips, so the only edit left here is the hook's own import.
|
|
805
|
+
if (!unbindsReactSpecifiers) {
|
|
806
|
+
return insertImportFixes(fixer, unbindings);
|
|
807
|
+
}
|
|
781
808
|
// A surviving reference (or a member-only file) leaves the react
|
|
782
809
|
// import untouched; only the new import is added when missing.
|
|
783
810
|
if (hasSurvivingReference || specifiers.length === 0) {
|
|
784
|
-
|
|
785
|
-
return [];
|
|
786
|
-
}
|
|
787
|
-
return [fixer.insertTextBefore(statement, `${importText}\n`)];
|
|
811
|
+
return insertImportFixes(fixer, unbindings);
|
|
788
812
|
}
|
|
789
813
|
const defaultOrNamespace = statement.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
790
814
|
s.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
@@ -801,10 +825,9 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
801
825
|
}
|
|
802
826
|
return [fixer.remove(statement)];
|
|
803
827
|
}
|
|
804
|
-
const fixes = [
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
828
|
+
const fixes = [
|
|
829
|
+
...insertImportFixes(fixer, unbindings),
|
|
830
|
+
];
|
|
808
831
|
if (remainingNamed.length === 0) {
|
|
809
832
|
fixes.push(...removeNamedGroup(fixer, namedSpecifiers));
|
|
810
833
|
}
|
|
@@ -862,6 +885,55 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
862
885
|
conversion.node.range[1],
|
|
863
886
|
]);
|
|
864
887
|
const programScope = ASTHelpers_1.ASTHelpers.getScope(context, program);
|
|
888
|
+
/**
|
|
889
|
+
* The spans the batched rewrite genuinely erases.
|
|
890
|
+
*
|
|
891
|
+
* Only two parts of a converted call disappear: its callee, which the
|
|
892
|
+
* hook's name replaces, and everything past the callback argument — the
|
|
893
|
+
* dependency array and any further argument. The callback text and the
|
|
894
|
+
* call's type parameters are re-emitted verbatim, so listing them would
|
|
895
|
+
* read every binding they mention as unreferenced and unbind imports the
|
|
896
|
+
* rewritten call still uses.
|
|
897
|
+
*
|
|
898
|
+
* `includeIdentifierCallees` selects which oracle owns the react
|
|
899
|
+
* `useCallback` specifier. Excluded, the specifier's binding has no
|
|
900
|
+
* reference among these spans, so the orphan planner never considers it
|
|
901
|
+
* and {@link importFixes} keeps its comment-preserving splice.
|
|
902
|
+
*/
|
|
903
|
+
const erasedSpans = (includeIdentifierCallees) => batchedConversions.flatMap((conversion) => {
|
|
904
|
+
const { node } = conversion;
|
|
905
|
+
const spans = [
|
|
906
|
+
[node.arguments[0].range[1], node.range[1]],
|
|
907
|
+
];
|
|
908
|
+
// A member callee's object is the only binding it reads, and the
|
|
909
|
+
// rule's own import rewrite never touches it.
|
|
910
|
+
if (includeIdentifierCallees || !conversion.callee) {
|
|
911
|
+
spans.push(node.callee.range);
|
|
912
|
+
}
|
|
913
|
+
return spans;
|
|
914
|
+
});
|
|
915
|
+
/**
|
|
916
|
+
* Whether the edits form the single ordered, non-overlapping sequence
|
|
917
|
+
* ESLint demands of one fix.
|
|
918
|
+
*
|
|
919
|
+
* The two import oracles below own disjoint declarations in every shape
|
|
920
|
+
* this rule is written for, but "every shape" is a claim about a file
|
|
921
|
+
* nobody has seen. A file that breaks it — a dependency array reading
|
|
922
|
+
* the same react declaration the specifier splice rewrites — would make
|
|
923
|
+
* ESLint throw out of the whole lint rather than report, so the edit set
|
|
924
|
+
* is checked and withheld instead.
|
|
925
|
+
*/
|
|
926
|
+
const editsAreDisjoint = (fixes) => {
|
|
927
|
+
const ordered = [...fixes].sort((left, right) => left.range[0] - right.range[0] || left.range[1] - right.range[1]);
|
|
928
|
+
let previousEnd = -1;
|
|
929
|
+
for (const fix of ordered) {
|
|
930
|
+
if (fix.range[0] < previousEnd) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
previousEnd = fix.range[1];
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
};
|
|
865
937
|
// Every call-site conversion and the import rewrite ride on ONE fix
|
|
866
938
|
// from ONE report. ESLint discards a multi-part fix wholesale when any
|
|
867
939
|
// part conflicts with another rule's fix and retries it on the next
|
|
@@ -894,11 +966,37 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
|
|
|
894
966
|
if (orphansLocalBinding(programScope, droppedRanges)) {
|
|
895
967
|
return null;
|
|
896
968
|
}
|
|
969
|
+
// Converting every `React.useCallback` call strips the last read of
|
|
970
|
+
// the react default import, and dropping a dependency array strips
|
|
971
|
+
// the last read of whatever was computed for it. Either leaves the
|
|
972
|
+
// file failing `no-unused-vars` and `noUnusedLocals` on a fix that
|
|
973
|
+
// resolved the only report that would have re-raised the debt
|
|
974
|
+
// (issue #1898).
|
|
975
|
+
//
|
|
976
|
+
// Two plans, because the react `useCallback` specifier has two
|
|
977
|
+
// possible owners and only one of them may edit it. The wider plan
|
|
978
|
+
// decides everything, which is what unbinds a mixed
|
|
979
|
+
// `import React, { useCallback }` as a single declaration; when it
|
|
980
|
+
// declines — a comment among the specifiers, a name the planner's
|
|
981
|
+
// second-opinion scan still finds — the narrower plan hands that
|
|
982
|
+
// specifier back to the splice above, which preserves the comments
|
|
983
|
+
// the planner refuses to guess at (issue #1446).
|
|
984
|
+
//
|
|
985
|
+
// A null from BOTH is a demand to withhold: a binding is left
|
|
986
|
+
// unreferenced that neither oracle can unbind safely.
|
|
987
|
+
const wholePlan = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, erasedSpans(true));
|
|
988
|
+
const unbindings = wholePlan ??
|
|
989
|
+
(0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, erasedSpans(false));
|
|
990
|
+
if (!unbindings) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
897
993
|
const texts = conversionTexts();
|
|
898
|
-
|
|
994
|
+
const fixes = [
|
|
899
995
|
...batchedConversions.map((conversion) => fixer.replaceText(conversion.node, texts.get(conversion.node))),
|
|
900
|
-
...importFixes(fixer),
|
|
996
|
+
...importFixes(fixer, wholePlan === null, unbindings),
|
|
997
|
+
...unbindings.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
901
998
|
];
|
|
999
|
+
return editsAreDisjoint(fixes) ? fixes : null;
|
|
902
1000
|
},
|
|
903
1001
|
});
|
|
904
1002
|
for (const conversion of [...followers, ...deferredConversions]) {
|
|
@@ -95,16 +95,32 @@ function isFunctionExpressionLike(node) {
|
|
|
95
95
|
node.type === 'FunctionExpression' ||
|
|
96
96
|
node.type === 'FunctionDeclaration'));
|
|
97
97
|
}
|
|
98
|
+
// Find the function a declaration contributes to the ordering inventory.
|
|
99
|
+
//
|
|
100
|
+
// A sibling declarator is a FIX-safety question, not a detection one: bailing
|
|
101
|
+
// out of the extractor drops the statement from the inventory entirely, so the
|
|
102
|
+
// ordering violation goes unreported instead of merely unfixed (#1891).
|
|
103
|
+
//
|
|
104
|
+
// At most ONE binding per statement is surfaced — the first function-like
|
|
105
|
+
// declarator — because every downstream structure is keyed on statement
|
|
106
|
+
// identity: the expected-order slots, the comment-inclusive statement ranges,
|
|
107
|
+
// and the fixer's per-statement edits. A second entry sharing a statement would
|
|
108
|
+
// emit two edits over one range and duplicate that statement's text. A later
|
|
109
|
+
// function-like sibling therefore stays invisible, which under-constrains the
|
|
110
|
+
// order rather than misstating it, and no ordering instruction is ever emitted
|
|
111
|
+
// for two functions that cannot move independently of each other.
|
|
98
112
|
function extractFunctionFromVariableDeclaration(statement) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
for (const declarator of statement.declarations) {
|
|
114
|
+
if (declarator.id.type === 'Identifier' &&
|
|
115
|
+
isFunctionExpressionLike(declarator.init)) {
|
|
116
|
+
return {
|
|
117
|
+
name: declarator.id.name,
|
|
118
|
+
fnNode: declarator.init,
|
|
119
|
+
hasSiblingDeclarators: statement.declarations.length > 1,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
106
122
|
}
|
|
107
|
-
return
|
|
123
|
+
return null;
|
|
108
124
|
}
|
|
109
125
|
function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
110
126
|
const functions = [];
|
|
@@ -124,6 +140,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
|
124
140
|
isUtility: utilityRegex.test(name),
|
|
125
141
|
dependencies: [],
|
|
126
142
|
originalIndex: index,
|
|
143
|
+
hasSiblingDeclarators: false,
|
|
127
144
|
});
|
|
128
145
|
return;
|
|
129
146
|
}
|
|
@@ -142,13 +159,14 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
|
142
159
|
isUtility: utilityRegex.test(name),
|
|
143
160
|
dependencies: [],
|
|
144
161
|
originalIndex: index,
|
|
162
|
+
hasSiblingDeclarators: false,
|
|
145
163
|
});
|
|
146
164
|
return;
|
|
147
165
|
}
|
|
148
166
|
if (statement.declaration.type === 'VariableDeclaration') {
|
|
149
167
|
const extracted = extractFunctionFromVariableDeclaration(statement.declaration);
|
|
150
168
|
if (extracted) {
|
|
151
|
-
const { name, fnNode } = extracted;
|
|
169
|
+
const { name, fnNode, hasSiblingDeclarators } = extracted;
|
|
152
170
|
functions.push({
|
|
153
171
|
name,
|
|
154
172
|
fnNode,
|
|
@@ -158,6 +176,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
|
158
176
|
isUtility: utilityRegex.test(name),
|
|
159
177
|
dependencies: [],
|
|
160
178
|
originalIndex: index,
|
|
179
|
+
hasSiblingDeclarators,
|
|
161
180
|
});
|
|
162
181
|
}
|
|
163
182
|
}
|
|
@@ -177,13 +196,14 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
|
177
196
|
isUtility: utilityRegex.test(name),
|
|
178
197
|
dependencies: [],
|
|
179
198
|
originalIndex: index,
|
|
199
|
+
hasSiblingDeclarators: false,
|
|
180
200
|
});
|
|
181
201
|
return;
|
|
182
202
|
}
|
|
183
203
|
if (statement.type === 'VariableDeclaration') {
|
|
184
204
|
const extracted = extractFunctionFromVariableDeclaration(statement);
|
|
185
205
|
if (extracted) {
|
|
186
|
-
const { name, fnNode } = extracted;
|
|
206
|
+
const { name, fnNode, hasSiblingDeclarators } = extracted;
|
|
187
207
|
const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
|
|
188
208
|
functions.push({
|
|
189
209
|
name,
|
|
@@ -194,6 +214,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
|
194
214
|
isUtility: utilityRegex.test(name),
|
|
195
215
|
dependencies: [],
|
|
196
216
|
originalIndex: index,
|
|
217
|
+
hasSiblingDeclarators,
|
|
197
218
|
});
|
|
198
219
|
}
|
|
199
220
|
}
|
|
@@ -686,6 +707,16 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
686
707
|
firstFunctionIndex > lastFunctionIndex) {
|
|
687
708
|
return null;
|
|
688
709
|
}
|
|
710
|
+
// Both reorder paths rewrite whole statements, so moving a function
|
|
711
|
+
// that shares its declaration with other bindings would carry those
|
|
712
|
+
// siblings with it — or, split apart, would leave the sibling's
|
|
713
|
+
// initializer evaluating somewhere it was never written to. Neither
|
|
714
|
+
// is a rewrite this rule is entitled to make on the reader's behalf.
|
|
715
|
+
// The misorderedFunction report still fires; only the rewrite of the
|
|
716
|
+
// region containing such a declaration is withheld.
|
|
717
|
+
if (functions.some((fn) => fn.hasSiblingDeclarators)) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
689
720
|
const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
|
|
690
721
|
const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
|
|
691
722
|
// Decline a reorder that would hoist a function above an interleaved
|
|
@@ -9,6 +9,27 @@ export type ImportBindingSpecifier = TSESTree.ImportSpecifier | TSESTree.ImportD
|
|
|
9
9
|
* run.
|
|
10
10
|
*/
|
|
11
11
|
export type ImportRemovalSource = Pick<TSESLint.SourceCode, 'text' | 'ast' | 'scopeManager' | 'getTokenBefore' | 'getTokenAfter' | 'getCommentsBefore' | 'getCommentsInside'>;
|
|
12
|
+
/**
|
|
13
|
+
* The slice to delete for a statement that loses its last binding. The whole
|
|
14
|
+
* line goes when the statement owns it, trailing same-line comments included:
|
|
15
|
+
* those describe the declaration that is going away, and a
|
|
16
|
+
* `// eslint-disable-line` among them would otherwise outlive its subject. A
|
|
17
|
+
* statement sharing its line with other code gives up only its own characters.
|
|
18
|
+
*/
|
|
19
|
+
export declare function statementRemovalRange(source: ImportRemovalSource, declaration: TSESTree.Node): TextRange | null;
|
|
20
|
+
export type Removable = {
|
|
21
|
+
range: TextRange;
|
|
22
|
+
removed: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Ranges covering every removed item together with the separators that would
|
|
26
|
+
* be left dangling. A removed run reaches forward to the next survivor, or
|
|
27
|
+
* backward from the previous one when it ends the list, so exactly one
|
|
28
|
+
* separator per removed item disappears. Returns `null` when every item is
|
|
29
|
+
* removed — that collapse is the caller's to make, since what encloses the
|
|
30
|
+
* list (braces, the declaration itself) must go too.
|
|
31
|
+
*/
|
|
32
|
+
export declare function removalRuns(items: readonly Removable[]): TextRange[] | null;
|
|
12
33
|
/**
|
|
13
34
|
* The text ranges that unbind `specifiers` from their shared declaration, or
|
|
14
35
|
* `null` when no removal is provably safe. All specifiers of the declaration
|
|
@@ -50,6 +71,58 @@ export type ImportBindingUse = {
|
|
|
50
71
|
* fix may claim that.
|
|
51
72
|
*/
|
|
52
73
|
export declare function importBindingReferences(source: ImportRemovalSource): ImportBindingUse[];
|
|
74
|
+
/**
|
|
75
|
+
* True when the name still occurs in the source outside `ranges` and outside
|
|
76
|
+
* `binding` — the span or spans that declare it — ignoring the positions where a
|
|
77
|
+
* name is a module's export name rather than a reference to a local binding.
|
|
78
|
+
*
|
|
79
|
+
* Scope analysis already answers "is this binding referenced", and it is what
|
|
80
|
+
* `no-unused-vars` answers with. This coarser second opinion exists to catch
|
|
81
|
+
* the cases where the two could disagree — a shadowing binding, a construct the
|
|
82
|
+
* scope manager does not link — because a removal that turns out to be wrong
|
|
83
|
+
* deletes working code, while a removal declined in error only leaves the
|
|
84
|
+
* original report standing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function nameOccursOutside(source: ImportRemovalSource, name: string, ranges: readonly TextRange[], binding: readonly TextRange[]): boolean;
|
|
87
|
+
/** Every binding identifier a destructuring pattern introduces. */
|
|
88
|
+
export declare function patternIdentifiers(pattern: TSESTree.Node | null): TSESTree.Identifier[];
|
|
89
|
+
/**
|
|
90
|
+
* Every binding that `removed` would leave with nothing referencing it.
|
|
91
|
+
*
|
|
92
|
+
* The scan answers only "which bindings does this deletion strand"; what to do
|
|
93
|
+
* about each is the caller's, since an import is unbound by deleting a specifier
|
|
94
|
+
* while a destructured property is unbound by deleting a property — and some
|
|
95
|
+
* bindings must not be touched at all.
|
|
96
|
+
*/
|
|
97
|
+
export declare function orphanedBindings(source: ImportRemovalSource, removed: readonly TextRange[]): TSESLint.Scope.Variable[];
|
|
98
|
+
/** A binding together with the ranges that USE it. */
|
|
99
|
+
export type BindingUse = {
|
|
100
|
+
variable: TSESLint.Scope.Variable;
|
|
101
|
+
uses: TextRange[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Every binding the file declares, paired with the positions that USE it — the
|
|
105
|
+
* same set {@link orphanedBindings} judges orphanhood by, a declarator's own
|
|
106
|
+
* initializer write excluded.
|
|
107
|
+
*
|
|
108
|
+
* {@link importBindingReferences} answers the narrower question for import
|
|
109
|
+
* bindings alone, which suffices only for a caller whose deletions can strand
|
|
110
|
+
* nothing else. A caller that can also strand a local declaration — a `type`
|
|
111
|
+
* alias named by the annotations it deletes, a `const` read through `typeof` —
|
|
112
|
+
* has to partition its edits over every binding, because a binding it never
|
|
113
|
+
* asks about ends up owned by no fix at all and stays orphaned for good.
|
|
114
|
+
*/
|
|
115
|
+
export declare function bindingUses(source: ImportRemovalSource): BindingUse[];
|
|
116
|
+
/**
|
|
117
|
+
* How a caller unbinds the orphans this module does not own — a destructured
|
|
118
|
+
* property, a local declaration, a binding it deliberately leaves alone.
|
|
119
|
+
*
|
|
120
|
+
* Every such orphan arrives in ONE call because two of them can share a list
|
|
121
|
+
* whose separators are only correct when planned together. The result is the
|
|
122
|
+
* ranges that unbind them, `[]` for orphans the caller accepts as they are, or
|
|
123
|
+
* `null` to decline — which withholds the caller's whole fix.
|
|
124
|
+
*/
|
|
125
|
+
export type OrphanUnbinder = (variables: readonly TSESLint.Scope.Variable[], removed: readonly TextRange[]) => TextRange[] | null;
|
|
53
126
|
/**
|
|
54
127
|
* The extra ranges a fix must delete so that removing `removed` leaves nothing
|
|
55
128
|
* bound to nothing, or `null` when some binding would be orphaned yet cannot be
|
|
@@ -78,3 +151,18 @@ export declare function importBindingReferences(source: ImportRemovalSource): Im
|
|
|
78
151
|
* to work out which edits belong in the same batch.
|
|
79
152
|
*/
|
|
80
153
|
export declare function planOrphanedImportRemoval(source: ImportRemovalSource, removed: readonly TextRange[]): TextRange[] | null;
|
|
154
|
+
/**
|
|
155
|
+
* {@link planOrphanedImportRemoval} widened by an `unbind` strategy for the
|
|
156
|
+
* orphans imports do not cover. Without one, an orphan bound by anything other
|
|
157
|
+
* than an import declines the fix, which is exactly what the import-only entry
|
|
158
|
+
* point promises.
|
|
159
|
+
*
|
|
160
|
+
* Every contract of {@link planOrphanedImportRemoval} carries over unchanged —
|
|
161
|
+
* `removed` is one fix's own deletion, `null` is a demand — with one addition
|
|
162
|
+
* the import arm never needed. An import declaration binds names without reading
|
|
163
|
+
* any, so unbinding one cannot strand a second binding; a declaration `unbind`
|
|
164
|
+
* deletes usually DOES read something (`const { data } = event` reads `event`),
|
|
165
|
+
* so its plan is re-examined against the file and declined if it strands
|
|
166
|
+
* anything the first pass did not already account for.
|
|
167
|
+
*/
|
|
168
|
+
export declare function planOrphanedBindingRemoval(source: ImportRemovalSource, removed: readonly TextRange[], unbind?: OrphanUnbinder): TextRange[] | null;
|