@blumintinc/eslint-plugin-blumint 1.20.144 → 1.20.146

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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.144',
226
+ version: '1.20.146',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -196,6 +196,246 @@ function declaresFunctionParameter(params) {
196
196
  }
197
197
  return parameterAnnotation(params[0])?.type === utils_1.AST_NODE_TYPES.TSFunctionType;
198
198
  }
199
+ /**
200
+ * A database transaction handle is valid only for the attempt that created it,
201
+ * and a transaction body is re-run whenever the driver retries — Firestore
202
+ * retries an attempt whose reads a concurrent write invalidated. A memoized
203
+ * body hands the retry the first attempt's cached promise, so the retry queues
204
+ * no writes on its own handle, commits empty, and the caller reads the first
205
+ * attempt's return value and reports success. Memoizing the method that OWNS
206
+ * the transaction is the same defect one level up: the whole transaction, writes
207
+ * included, then runs once per instance.
208
+ */
209
+ const TRANSACTION_TYPE_NAME = 'Transaction';
210
+ const RUN_TRANSACTION_NAME = 'runTransaction';
211
+ /**
212
+ * Keys that hold source positions or the tree's only back-edge rather than
213
+ * child nodes; `parent` would make a subtree walk non-terminating.
214
+ */
215
+ const NON_TRAVERSABLE_KEYS = new Set(['parent', 'range', 'loc', 'type']);
216
+ /** Every node of `root`'s subtree, `root` included. */
217
+ function* subtreeOf(root) {
218
+ yield root;
219
+ for (const [key, value] of Object.entries(root)) {
220
+ if (NON_TRAVERSABLE_KEYS.has(key)) {
221
+ continue;
222
+ }
223
+ if (Array.isArray(value)) {
224
+ for (const element of value) {
225
+ if (ASTHelpers_1.ASTHelpers.isNode(element)) {
226
+ yield* subtreeOf(element);
227
+ }
228
+ }
229
+ }
230
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
231
+ yield* subtreeOf(value);
232
+ }
233
+ }
234
+ }
235
+ /** The name a call invokes, for `f()` and for `o.f()` alike. */
236
+ function calleeName(node) {
237
+ const callee = withoutChain(node.callee);
238
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
239
+ return callee.name;
240
+ }
241
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
242
+ !callee.computed &&
243
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
244
+ return callee.property.name;
245
+ }
246
+ return undefined;
247
+ }
248
+ /**
249
+ * Whether the node is a `runTransaction(…)` call, under any receiver:
250
+ * `db.runTransaction`, `firestore.runTransaction` and a bare imported
251
+ * `runTransaction` all open a retryable transaction.
252
+ */
253
+ function isRunTransactionCall(node) {
254
+ return (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
255
+ calleeName(node) === RUN_TRANSACTION_NAME);
256
+ }
257
+ /** Whether the method opens a transaction anywhere in its own body. */
258
+ function ownsTransaction(fn) {
259
+ for (const node of subtreeOf(fn)) {
260
+ if (isRunTransactionCall(node)) {
261
+ return true;
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+ /**
267
+ * Whether a type name denotes the transaction handle. The rightmost segment is
268
+ * the type's own name, so the qualified spellings — `FirebaseFirestore.
269
+ * Transaction`, `admin.firestore.Transaction` — answer alongside the bare one;
270
+ * a locally aliased import (`import { Transaction as Txn }`) answers through
271
+ * the alias set the file's imports define.
272
+ */
273
+ function namesTransaction(typeName, aliases) {
274
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
275
+ return (typeName.name === TRANSACTION_TYPE_NAME || aliases.has(typeName.name));
276
+ }
277
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
278
+ return typeName.right.name === TRANSACTION_TYPE_NAME;
279
+ }
280
+ return false;
281
+ }
282
+ /**
283
+ * Whether a declared type hands the method a transaction handle: written
284
+ * directly, as one arm of a union or intersection, or as a property of an
285
+ * object type — the shape a destructured `{ transaction }: { transaction:
286
+ * Transaction }` parameter carries.
287
+ *
288
+ * Type ARGUMENTS are deliberately not entered: `Map<string, Transaction>` or
289
+ * `Promise<Transaction>` describes a collection of handles or a handle yet to
290
+ * exist, neither of which is the attempt-scoped handle this carve-out is about.
291
+ */
292
+ function declaresTransactionType(annotation, aliases) {
293
+ if (!annotation) {
294
+ return false;
295
+ }
296
+ if (annotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
297
+ return namesTransaction(annotation.typeName, aliases);
298
+ }
299
+ if (annotation.type === utils_1.AST_NODE_TYPES.TSUnionType ||
300
+ annotation.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
301
+ return annotation.types.some((member) => declaresTransactionType(member, aliases));
302
+ }
303
+ if (annotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
304
+ return annotation.members.some((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
305
+ declaresTransactionType(member.typeAnnotation?.typeAnnotation, aliases));
306
+ }
307
+ return false;
308
+ }
309
+ /**
310
+ * The type annotation a parameter declares, including the destructuring shapes
311
+ * `parameterAnnotation` does not reach: an object or array pattern carries its
312
+ * annotation on the pattern itself.
313
+ */
314
+ function bindingAnnotation(param) {
315
+ if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
316
+ param.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
317
+ return param.typeAnnotation?.typeAnnotation;
318
+ }
319
+ return parameterAnnotation(param);
320
+ }
321
+ /**
322
+ * Whether the method declares a parameter typed as a transaction handle.
323
+ *
324
+ * The test reads the ANNOTATION, not the parameter's name: a parameter merely
325
+ * named `transaction` is as likely to hold a payment or a ledger entry, and the
326
+ * rule's other carve-outs (void result, callback parameter) are annotation-driven
327
+ * for the same reason. A bare `async apply(transaction)` therefore keeps
328
+ * reporting — it declares nothing to honour, and under the `noImplicitAny` its
329
+ * consumers compile with it does not type-check anyway. Where the handle arrives
330
+ * through an unresolvable alias (`args: MembershipArgs`), the call-site test
331
+ * below is what recognises it.
332
+ */
333
+ function declaresTransactionParameter(params, aliases) {
334
+ return params.some((param) => declaresTransactionType(bindingAnnotation(param), aliases));
335
+ }
336
+ /**
337
+ * The expression itself, with any `ChainExpression` wrapper removed. ESTree
338
+ * wraps a whole optional chain in that node, so `this?.body` and
339
+ * `this.body.bind?.(this)` reach a bare member/call test as something else
340
+ * entirely. Nullish spellings carry the transaction handle exactly as the plain
341
+ * ones do, and reading through the wrapper is what keeps the carve-out from
342
+ * lapsing on them — a lapse that would restore the empty-commit autofix.
343
+ */
344
+ function withoutChain(node) {
345
+ return node.type === utils_1.AST_NODE_TYPES.ChainExpression
346
+ ? withoutChain(node.expression)
347
+ : node;
348
+ }
349
+ /** The own-method name a `this.foo` reference reads, if it reads one. */
350
+ function thisMemberName(node) {
351
+ const expression = withoutChain(node);
352
+ if (expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
353
+ !expression.computed &&
354
+ withoutChain(expression.object).type === utils_1.AST_NODE_TYPES.ThisExpression &&
355
+ expression.property.type === utils_1.AST_NODE_TYPES.Identifier) {
356
+ return expression.property.name;
357
+ }
358
+ // `this.body.bind(this)` passes the same method, one wrapper out.
359
+ if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
360
+ expression.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
361
+ !expression.callee.computed &&
362
+ expression.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
363
+ expression.callee.property.name === 'bind') {
364
+ return thisMemberName(expression.callee.object);
365
+ }
366
+ return undefined;
367
+ }
368
+ /** Whether the subtree mentions the binding, under any nesting. */
369
+ function mentionsBinding(node, name) {
370
+ for (const descendant of subtreeOf(node)) {
371
+ if (descendant.type === utils_1.AST_NODE_TYPES.Identifier &&
372
+ descendant.name === name) {
373
+ return true;
374
+ }
375
+ }
376
+ return false;
377
+ }
378
+ /**
379
+ * Own methods a `runTransaction` argument hands the attempt to: the method
380
+ * passed as the callback itself, and every `this.method(…)` the callback body
381
+ * invokes with the attempt's handle among its arguments.
382
+ *
383
+ * Passing the handle on is what makes a method part of the attempt, so a
384
+ * helper the callback calls WITHOUT it — a config read, a lookup that takes no
385
+ * transaction — is untouched and keeps reporting.
386
+ */
387
+ function collectTransactionParticipants(argument, participants) {
388
+ const passed = thisMemberName(argument);
389
+ if (passed) {
390
+ participants.add(passed);
391
+ return;
392
+ }
393
+ if (argument.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
394
+ argument.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
395
+ return;
396
+ }
397
+ const handle = argument.params[0];
398
+ if (handle?.type !== utils_1.AST_NODE_TYPES.Identifier) {
399
+ return;
400
+ }
401
+ for (const node of subtreeOf(argument.body)) {
402
+ if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
403
+ continue;
404
+ }
405
+ const method = thisMemberName(node.callee);
406
+ if (method &&
407
+ node.arguments.some((arg) => mentionsBinding(arg, handle.name))) {
408
+ participants.add(method);
409
+ }
410
+ }
411
+ }
412
+ /** Every own method the class hands a transaction handle to. */
413
+ function transactionParticipantsOf(body) {
414
+ const participants = new Set();
415
+ for (const node of subtreeOf(body)) {
416
+ if (!isRunTransactionCall(node)) {
417
+ continue;
418
+ }
419
+ for (const argument of node.arguments) {
420
+ collectTransactionParticipants(argument, participants);
421
+ }
422
+ }
423
+ return participants;
424
+ }
425
+ /** The statically known name of a method, for matching call sites against it. */
426
+ function methodName(node) {
427
+ if (node.computed) {
428
+ return undefined;
429
+ }
430
+ const { key } = node;
431
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
432
+ return key.name;
433
+ }
434
+ if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
435
+ return key.value;
436
+ }
437
+ return undefined;
438
+ }
199
439
  /**
200
440
  * Matches a memoize decorator in supported syntaxes:
201
441
  * - @Alias()
@@ -290,6 +530,66 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
290
530
  }
291
531
  return memoizeImportCache;
292
532
  };
533
+ /**
534
+ * Local names bound to an imported `Transaction` type, so that an aliased
535
+ * import (`import { Transaction as Txn } from 'firebase-admin/firestore'`)
536
+ * is read as the handle it is. The module is not constrained: a handle is
537
+ * re-exported through as many paths as a codebase has layers, and the
538
+ * imported NAME already carries the signal.
539
+ */
540
+ let transactionAliasCache = null;
541
+ const transactionAliases = () => {
542
+ if (!transactionAliasCache) {
543
+ transactionAliasCache = new Set();
544
+ for (const statement of context.sourceCode.ast.body) {
545
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
546
+ continue;
547
+ }
548
+ for (const spec of statement.specifiers) {
549
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
550
+ spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
551
+ spec.imported.name === TRANSACTION_TYPE_NAME) {
552
+ transactionAliasCache.add(spec.local.name);
553
+ }
554
+ }
555
+ }
556
+ }
557
+ return transactionAliasCache;
558
+ };
559
+ /**
560
+ * The class-level scan is shared by every method of the class, so it runs
561
+ * once per class body rather than once per candidate method.
562
+ */
563
+ const participantCache = new WeakMap();
564
+ const transactionParticipants = (body) => {
565
+ let participants = participantCache.get(body);
566
+ if (!participants) {
567
+ participants = transactionParticipantsOf(body);
568
+ participantCache.set(body, participants);
569
+ }
570
+ return participants;
571
+ };
572
+ /**
573
+ * Whether the method takes part in a database transaction attempt, either
574
+ * by opening one or by being handed the attempt's handle. Caching such a
575
+ * method is never an optimisation: a retried attempt would replay the first
576
+ * attempt's promise, writing nothing on its own handle while the caller
577
+ * reads a success it did not get.
578
+ */
579
+ const participatesInTransaction = (node, fn) => {
580
+ if (declaresTransactionParameter(fn.params, transactionAliases())) {
581
+ return true;
582
+ }
583
+ if (ownsTransaction(fn)) {
584
+ return true;
585
+ }
586
+ const body = node.parent;
587
+ if (body?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
588
+ return false;
589
+ }
590
+ const name = methodName(node);
591
+ return name !== undefined && transactionParticipants(body).has(name);
592
+ };
293
593
  return {
294
594
  MethodDefinition(node) {
295
595
  // Only process async instance methods (skip static methods)
@@ -341,6 +641,17 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
341
641
  if (declaresVoidResult(node.value.returnType)) {
342
642
  return;
343
643
  }
644
+ // A transaction handle is valid only for the attempt that created it,
645
+ // so a result derived from one must not outlive that attempt. Caching
646
+ // the body of a `runTransaction` callback — or the method that owns the
647
+ // call — turns the retry a concurrent write provokes into a silent
648
+ // no-op: the retry replays the first attempt's promise, queues nothing
649
+ // on its own handle, commits empty, and reports the first attempt's
650
+ // value as success. The fixer would apply that unattended under
651
+ // `--fix`, so both report and fix are withheld.
652
+ if (participatesInTransaction(node, node.value)) {
653
+ return;
654
+ }
344
655
  const { aliases: memoizeAliases, namespaces: memoizeNamespaces } = memoizeImports();
345
656
  const hasMemoizeImport = memoizeAliases.size > 0 || memoizeNamespaces.size > 0;
346
657
  // Check if method already has @Memoize or @Memoize() decorator
@@ -3,6 +3,81 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noAlwaysTrueFalseConditions = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const utils_1 = require("@typescript-eslint/utils");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
7
+ const BREAKABLE = new Set([
8
+ utils_1.AST_NODE_TYPES.WhileStatement,
9
+ utils_1.AST_NODE_TYPES.DoWhileStatement,
10
+ utils_1.AST_NODE_TYPES.ForStatement,
11
+ utils_1.AST_NODE_TYPES.ForInStatement,
12
+ utils_1.AST_NODE_TYPES.ForOfStatement,
13
+ utils_1.AST_NODE_TYPES.SwitchStatement,
14
+ ]);
15
+ /**
16
+ * Whether `body` can leave the loop that owns it.
17
+ *
18
+ * An unlabeled `break` binds to the nearest enclosing loop or switch, so a
19
+ * nested breakable is still walked but its own unlabeled breaks do not count; a
20
+ * labeled one counts when it names this loop. `return` and `throw` leave the
21
+ * loop as well, unless they sit inside a nested function, which has its own.
22
+ */
23
+ function canExitLoop(body, label) {
24
+ let escapes = false;
25
+ const visit = (node, insideNestedBreakable) => {
26
+ if (escapes)
27
+ return;
28
+ switch (node.type) {
29
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
30
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
31
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
32
+ return;
33
+ case utils_1.AST_NODE_TYPES.BreakStatement:
34
+ if (node.label ? node.label.name === label : !insideNestedBreakable) {
35
+ escapes = true;
36
+ }
37
+ return;
38
+ case utils_1.AST_NODE_TYPES.ReturnStatement:
39
+ case utils_1.AST_NODE_TYPES.ThrowStatement:
40
+ escapes = true;
41
+ return;
42
+ default:
43
+ break;
44
+ }
45
+ const nested = insideNestedBreakable || BREAKABLE.has(node.type);
46
+ for (const [key, value] of Object.entries(node)) {
47
+ if (key === 'parent')
48
+ continue;
49
+ if (Array.isArray(value)) {
50
+ for (const child of value) {
51
+ if (ASTHelpers_1.ASTHelpers.isNode(child))
52
+ visit(child, nested);
53
+ }
54
+ }
55
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
56
+ visit(value, nested);
57
+ }
58
+ }
59
+ };
60
+ visit(body, false);
61
+ return escapes;
62
+ }
63
+ /**
64
+ * `while (true)` and `do … while (true)` around a `break` are how a loop whose
65
+ * exit is only known mid-body is written — cursor pagination being the usual
66
+ * case. The condition is the point, and unlike an `if` it cannot be removed:
67
+ * the only way to satisfy the report is to rewrite the loop as `for (;;)`,
68
+ * which this rule already accepts because it has no test node to check. A
69
+ * literal `true` over a body with no way out is still reported, since that loop
70
+ * really does run forever (#1973).
71
+ */
72
+ function isDeliberateInfiniteLoop(loop) {
73
+ if (loop.test?.type !== utils_1.AST_NODE_TYPES.Literal || loop.test.value !== true) {
74
+ return false;
75
+ }
76
+ const label = loop.parent?.type === utils_1.AST_NODE_TYPES.LabeledStatement
77
+ ? loop.parent.label.name
78
+ : null;
79
+ return canExitLoop(loop.body, label);
80
+ }
6
81
  exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
7
82
  name: 'no-always-true-false-conditions',
8
83
  meta: {
@@ -1498,15 +1573,19 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1498
1573
  },
1499
1574
  // Check while loops
1500
1575
  WhileStatement(node) {
1576
+ if (isDeliberateInfiniteLoop(node))
1577
+ return;
1501
1578
  checkCondition(node.test);
1502
1579
  },
1503
1580
  // Check do-while loops
1504
1581
  DoWhileStatement(node) {
1582
+ if (isDeliberateInfiniteLoop(node))
1583
+ return;
1505
1584
  checkCondition(node.test);
1506
1585
  },
1507
1586
  // Check for loop conditions
1508
1587
  ForStatement(node) {
1509
- if (node.test) {
1588
+ if (node.test && !isDeliberateInfiniteLoop(node)) {
1510
1589
  checkCondition(node.test);
1511
1590
  }
1512
1591
  },
@@ -8,6 +8,7 @@ const importRemoval_1 = require("../utils/importRemoval");
8
8
  const typeDeclarationRemoval_1 = require("../utils/typeDeclarationRemoval");
9
9
  const replacementSegments_1 = require("../utils/replacementSegments");
10
10
  const lexicalScope_1 = require("../utils/lexicalScope");
11
+ const arrowAnnotationGap_1 = require("../utils/arrowAnnotationGap");
11
12
  const defaultOptions = {
12
13
  allowRecursiveFunctions: true,
13
14
  allowOverloadedFunctions: true,
@@ -780,25 +781,6 @@ function batchAnnotations(source, candidates) {
780
781
  });
781
782
  return [...batches.values()];
782
783
  }
783
- /**
784
- * A comment whose meaning is tied to where it sits. Re-emitting one somewhere
785
- * else retargets it — a disable directive lands on an unrelated line and a
786
- * `@ts-expect-error` becomes an error of its own — so a removal that would move
787
- * one is withheld instead.
788
- */
789
- function isPositionalDirective(comment) {
790
- if ((0, disableDirectives_1.parseDisableDirectives)([comment]).length > 0) {
791
- return true;
792
- }
793
- const value = comment.value.trim();
794
- return value.startsWith('@ts-expect-error') || value.startsWith('@ts-ignore');
795
- }
796
- /** The indentation of the line `offset` sits on, for a carried line break. */
797
- function indentAt(source, offset) {
798
- const lineStart = source.text.lastIndexOf('\n', offset - 1) + 1;
799
- const [indent] = /^[ \t]*/.exec(source.text.slice(lineStart, offset)) ?? [''];
800
- return indent;
801
- }
802
784
  /**
803
785
  * Whether the span deletes a whole declaration of the program.
804
786
  *
@@ -827,9 +809,9 @@ function carriedText(source, range) {
827
809
  .filter((comment) => comment.range[0] >= range[0] && comment.range[1] <= range[1]);
828
810
  if (comments.length === 0)
829
811
  return '';
830
- if (comments.some(isPositionalDirective))
812
+ if (comments.some(arrowAnnotationGap_1.isPositionalDirective))
831
813
  return null;
832
- const indent = indentAt(source, range[0]);
814
+ const indent = (0, arrowAnnotationGap_1.indentAt)(source, range[0]);
833
815
  const segments = comments.map((comment) => ({
834
816
  text: source.text.slice(comment.range[0], comment.range[1]),
835
817
  breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
@@ -846,70 +828,20 @@ function carriedText(source, range) {
846
828
  : ' ';
847
829
  return `${lead}${body}${trail}`;
848
830
  }
849
- /** Every character the syntactic grammar counts as a LineTerminator. */
850
- const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
851
- const textOf = (source, range) => source.text.slice(range[0], range[1]);
852
- /**
853
- * The span an arrow's return annotation occupies between the parameter list and
854
- * the `=>`, together with that arrow token.
855
- *
856
- * The span holds the annotation, whitespace and comments and nothing else,
857
- * which is what makes it safe to rewrite wholesale: no binding reference can
858
- * hide in it beyond the ones the annotation itself names.
859
- */
860
- function arrowAnnotationGap(source, returnType) {
861
- const parametersEnd = source.getTokenBefore(returnType);
862
- const arrow = source.getTokenAfter(returnType, {
863
- filter: (token) => token.value === '=>',
864
- });
865
- if (!parametersEnd || !arrow)
866
- return null;
867
- const gap = [parametersEnd.range[1], arrow.range[0]];
868
- return containsRange(gap, returnType.range) ? { gap, arrow } : null;
869
- }
870
- /**
871
- * Re-emits `comments` on the far side of the arrow, where a line terminator is
872
- * inert, consuming the horizontal whitespace the arrow already had after it so
873
- * the body keeps a single separator.
874
- */
875
- function hoistPastArrow(source, arrow, comments) {
876
- const indent = indentAt(source, arrow.range[0]);
877
- const trailingText = source.text.slice(arrow.range[1]);
878
- const [spacing] = /^[ \t]*/.exec(trailingText) ?? [''];
879
- const body = (0, replacementSegments_1.joinSegmentBody)(comments.map((comment) => ({
880
- text: textOf(source, comment.range),
881
- breakAfter: true,
882
- })), indent);
883
- const rest = trailingText.slice(spacing.length);
884
- const separator = LINE_TERMINATOR.test(rest.charAt(0))
885
- ? ''
886
- : (0, replacementSegments_1.requiresLineBreakAfter)(comments[comments.length - 1])
887
- ? `\n${indent}`
888
- : ' ';
889
- return {
890
- range: [arrow.range[1], arrow.range[1] + spacing.length],
891
- text: ` ${body}${separator}`,
892
- };
893
- }
894
831
  /**
895
832
  * The edits that strip one annotation, carrying every comment the strip
896
833
  * strands rather than deleting it (#1877). `null` withholds the fix, for a
897
834
  * comment whose meaning is its position and which cannot stay where it is.
898
835
  *
899
836
  * An arrow is the one subject whose annotation sits inside a restricted
900
- * production: `ArrowParameters [no LineTerminator here] =>` forbids a line
901
- * terminator between the parameter list and the arrow, and a block comment
902
- * carrying a line terminator IS one to the grammar. A comment left there — or
903
- * carried there from inside the annotation — therefore turns the output into a
904
- * hard SyntaxError that only V8 reports, since `@typescript-eslint/parser`
905
- * accepts it (#1964). Such a comment is re-emitted past the `=>` instead, the
906
- * nearest position outside the restricted gap that cannot itself begin one;
907
- * hoisting it above the enclosing line would anchor an insertion at a column
908
- * zero that may sit inside a template literal or JSX text, where the comment
909
- * would become content rather than code.
837
+ * production, so its edits come from the shared planner that answers for that
838
+ * grammar (#1964). The removal span handed to it is the annotation's own
839
+ * range: unlike the planner's other caller, nothing here reaches back over the
840
+ * whitespace ahead of the `:`.
910
841
  *
911
842
  * Every other subject ends its parameter list at a body or a semicolon, so its
912
- * stranded comments stay where they were written.
843
+ * stranded comments stay where they were written and a deletion that carries
844
+ * them in place is correct.
913
845
  */
914
846
  function planAnnotationEdits(source, entry) {
915
847
  const range = entry.returnType.range;
@@ -917,53 +849,7 @@ function planAnnotationEdits(source, entry) {
917
849
  const carried = carriedText(source, range);
918
850
  return carried === null ? null : [{ range, text: carried }];
919
851
  }
920
- const gapInfo = arrowAnnotationGap(source, entry.returnType);
921
- if (!gapInfo)
922
- return null;
923
- const { gap, arrow } = gapInfo;
924
- const comments = source
925
- .getAllComments()
926
- .filter((comment) => containsRange(gap, comment.range));
927
- const stranded = comments.filter((comment) => containsRange(range, comment.range));
928
- // What the plain deletion would leave between the parameters and the arrow.
929
- // A comment left there contributes its own text, so a line comment or a
930
- // multi-line block comment shows up here as the line terminator it is.
931
- const residue = `${textOf(source, [gap[0], range[0]])}${textOf(source, [
932
- range[1],
933
- gap[1],
934
- ])}`;
935
- // The plain deletion is kept wherever it already lands a legal gap and
936
- // strands nothing, so no output that survives today moves by a byte.
937
- if (stranded.length === 0 && !LINE_TERMINATOR.test(residue)) {
938
- return [{ range, text: '' }];
939
- }
940
- // Rewriting the gap collapses the lines it spanned, which moves the line a
941
- // directive inside it points at, so the whole fix is withheld rather than
942
- // retargeting one. The gap a directive can share with nothing else is left
943
- // untouched by the branch above.
944
- if (comments.some(isPositionalDirective))
945
- return null;
946
- const hoisted = comments.filter(replacementSegments_1.requiresOwnLine);
947
- const inline = comments
948
- .filter((comment) => !(0, replacementSegments_1.requiresOwnLine)(comment))
949
- .map((comment) => textOf(source, comment.range));
950
- const edits = [
951
- { range: gap, text: inline.length === 0 ? ' ' : ` ${inline.join(' ')} ` },
952
- ];
953
- if (hoisted.length > 0) {
954
- edits.push(hoistPastArrow(source, arrow, hoisted));
955
- }
956
- return edits;
957
- }
958
- /**
959
- * ESLint applies a fix whole or not at all, and rejects one whose edits
960
- * overlap. Two spans planned independently — an annotation and the declaration
961
- * that strands it — can only overlap if a premise here is wrong, so an overlap
962
- * withdraws the fix rather than throwing at apply time.
963
- */
964
- function isDisjoint(edits) {
965
- const sorted = [...edits].sort((left, right) => left.range[0] - right.range[0]);
966
- return sorted.every((edit, index) => index === 0 || sorted[index - 1].range[1] <= edit.range[0]);
852
+ return (0, arrowAnnotationGap_1.planArrowAnnotationEdits)(source, entry.returnType, range);
967
853
  }
968
854
  /**
969
855
  * The edits a single fix makes for `batch`: the annotations themselves plus the
@@ -1002,7 +888,7 @@ function planRemoval(source, removalSource, batch) {
1002
888
  return null;
1003
889
  edits.push({ range, text: carried });
1004
890
  }
1005
- return isDisjoint(edits) ? edits : null;
891
+ return (0, arrowAnnotationGap_1.isDisjoint)(edits) ? edits : null;
1006
892
  }
1007
893
  exports.noExplicitReturnType = (0, createRule_1.createRule)({
1008
894
  name: 'no-explicit-return-type',