@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.
Files changed (28) hide show
  1. package/lib/index.js +1 -1
  2. package/lib/rules/enforce-centralized-mock-firestore.js +46 -3
  3. package/lib/rules/enforce-firestore-set-merge.js +278 -66
  4. package/lib/rules/enforce-microdiff.js +87 -20
  5. package/lib/rules/fast-deep-equal-over-microdiff.js +220 -71
  6. package/lib/rules/key-only-outermost-element.js +159 -21
  7. package/lib/rules/logical-top-to-bottom-grouping.js +104 -33
  8. package/lib/rules/no-empty-dependency-use-callbacks.js +116 -43
  9. package/lib/rules/no-explicit-return-type.js +164 -21
  10. package/lib/rules/no-mock-firebase-admin.js +52 -8
  11. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +2 -1
  12. package/lib/rules/no-redundant-usecallback-wrapper.js +126 -70
  13. package/lib/rules/no-unused-props.js +69 -57
  14. package/lib/rules/no-useless-usememo-primitives.d.ts +2 -1
  15. package/lib/rules/no-useless-usememo-primitives.js +156 -47
  16. package/lib/rules/no-usememo-for-pass-by-value.js +165 -220
  17. package/lib/rules/prefer-params-over-parent-id.d.ts +2 -1
  18. package/lib/rules/prefer-params-over-parent-id.js +122 -39
  19. package/lib/rules/use-latest-callback.js +109 -11
  20. package/lib/rules/vertically-group-related-functions.js +41 -10
  21. package/lib/utils/importRemoval.d.ts +88 -0
  22. package/lib/utils/importRemoval.js +127 -46
  23. package/lib/utils/patternBindingRemoval.d.ts +28 -0
  24. package/lib/utils/patternBindingRemoval.js +199 -0
  25. package/lib/utils/typeDeclarationRemoval.d.ts +32 -0
  26. package/lib/utils/typeDeclarationRemoval.js +105 -0
  27. package/package.json +1 -1
  28. 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
- const handlerNode = findHandlerFunction(node, handlerNodes);
235
- if (handlerNode) {
236
- const hasOptional = hasOptionalChaining(node);
237
- const paramsInScope = getParamsInScope(handlerNode);
238
- if (!paramsInScope) {
239
- context.report({
240
- node,
241
- messageId: 'preferParams',
242
- data: {
243
- paramName: getParentParamName(parentAccess.depth),
244
- },
245
- });
246
- return;
247
- }
248
- const paramSuggestion = getParentParamName(parentAccess.depth);
249
- let replacement = null;
250
- if (paramsInScope.identifier) {
251
- replacement = hasOptional
252
- ? `${paramsInScope.identifier}?.${paramSuggestion}`
253
- : `${paramsInScope.identifier}.${paramSuggestion}`;
254
- }
255
- else if (paramsInScope.properties) {
256
- const localName = paramsInScope.properties.get(paramSuggestion);
257
- if (localName) {
258
- replacement = localName;
259
- }
260
- }
261
- context.report({
262
- node,
263
- messageId: 'preferParams',
264
- data: {
265
- paramName: paramSuggestion,
266
- },
267
- fix: replacement
268
- ? (fixer) => fixer.replaceText(node, replacement)
269
- : undefined,
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
- const importFixes = (fixer) => {
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
- if (latestCallbackImport) {
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
- if (!latestCallbackImport) {
806
- fixes.push(fixer.insertTextBefore(statement, `${importText}\n`));
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
- return [
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
- if (statement.declarations.length !== 1) {
100
- return null;
101
- }
102
- const declarator = statement.declarations[0];
103
- if (declarator.id.type !== 'Identifier' ||
104
- !isFunctionExpressionLike(declarator.init)) {
105
- return null;
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 { name: declarator.id.name, fnNode: declarator.init };
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;