@blumintinc/eslint-plugin-blumint 1.20.173 → 1.20.174

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.173',
226
+ version: '1.20.174',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -685,7 +685,8 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
685
685
  }
686
686
  /**
687
687
  * The edits that add `appended` to the end of a call's argument list, laid
688
- * out the way the consumer's formatter prints the result.
688
+ * out the way the consumer's formatter prints the result — or `null` for
689
+ * the one tail no edit can extend without retargeting a directive.
689
690
  *
690
691
  * Neither layout re-emits an argument from its text. The broken one
691
692
  * rewrites the SEPARATORS between the arguments and shifts the indentation
@@ -708,16 +709,36 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
708
709
  return inline();
709
710
  }
710
711
  const { openParen, closeParen, spans } = layout;
711
- const tail = spans[spans.length - 1];
712
- // Between the last span and the closing parenthesis is the one gap the
713
- // spans do not absorb, because a trailing comma may sit there. A comment
714
- // past it would be inside a replaced range, so the list is left as it is
715
- // and the option appended in place instead of deleting one.
716
- if (sourceCode
717
- .getCommentsInside(node)
718
- .some((comment) => comment.range[0] >= tail.end)) {
712
+ // Between the last argument's own last token and the closing parenthesis
713
+ // sit the trailing comma, if one was written, and any comment. The tail
714
+ // SPAN absorbs such a comment whenever no comma follows it, so the span's
715
+ // end is not a safe place to write a separator: a `,` emitted after a
716
+ // line comment is swallowed into the comment's text and the call no
717
+ // longer parses (#2140). Both boundaries are therefore taken from the
718
+ // TOKEN stream, where a comment can never be the answer.
719
+ const beforeClose = sourceCode.getTokenBefore(closeParen);
720
+ const lastArgumentToken = beforeClose?.value === ','
721
+ ? sourceCode.getTokenBefore(beforeClose)
722
+ : beforeClose;
723
+ if (!beforeClose || !lastArgumentToken) {
719
724
  return inline();
720
725
  }
726
+ const gapComments = sourceCode
727
+ .getCommentsInside(node)
728
+ .filter((comment) => comment.range[0] >= lastArgumentToken.range[1]);
729
+ const sameLineComments = gapComments.filter((comment) => comment.loc.start.line === lastArgumentToken.loc.end.line);
730
+ const ownLineComments = gapComments.filter((comment) => comment.loc.start.line !== lastArgumentToken.loc.end.line);
731
+ // A directive trailing the argument's line means the line that FOLLOWS
732
+ // it: emitting anything there hands `{ merge: true }` the suppression
733
+ // that was written for the closing line, and re-exposes whatever it was
734
+ // suppressing. No layout preserves both the option's position and the
735
+ // directive's subject, so the fix is withheld and the report left to the
736
+ // developer, who restructures the call with the directive in view
737
+ // (#1877). A directive on a line of its OWN is safe: the option lands
738
+ // BEFORE it, so everything the directive is adjacent to stays adjacent.
739
+ if (sameLineComments.some(isPositionalDirective)) {
740
+ return null;
741
+ }
721
742
  // A broken list is indented one step past the line its parenthesis opens
722
743
  // on, whatever depth that line sits at, and closes at that line's own
723
744
  // column. A constant indent is right for exactly one call site.
@@ -740,9 +761,48 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
740
761
  fixes.push(fixer.replaceText(argument, relocated));
741
762
  }
742
763
  }
743
- fixes.push(fixer.replaceTextRange([tail.end, closeParen.range[0]], `${appended
744
- .map((argument) => `,\n${body}${argument}`)
745
- .join('')},\n${indent}`));
764
+ if (gapComments.length === 0) {
765
+ fixes.push(fixer.replaceTextRange([lastArgumentToken.range[1], closeParen.range[0]], `${appended
766
+ .map((argument) => `,\n${body}${argument}`)
767
+ .join('')},\n${indent}`));
768
+ return fixes;
769
+ }
770
+ const separatorPresent = beforeClose.value === ',';
771
+ const lastSameLine = sameLineComments[sameLineComments.length - 1];
772
+ if (ownLineComments.length === 0) {
773
+ // The comment trails the argument it annotates, so it keeps that line
774
+ // and the option opens the next one. The separator has to sit between
775
+ // the argument and the comment — after a line comment it is comment
776
+ // text — so one is inserted at the argument's own last token unless
777
+ // the author already wrote it, and everything through the end of the
778
+ // annotation is left byte for byte as written.
779
+ if (!separatorPresent) {
780
+ fixes.push(fixer.insertTextAfter(lastArgumentToken, ','));
781
+ }
782
+ const annotationEnd = Math.max(lastSameLine.range[1], beforeClose.range[1]);
783
+ fixes.push(fixer.replaceTextRange([annotationEnd, closeParen.range[0]], `${appended
784
+ .map((argument) => `\n${body}${argument},`)
785
+ .join('')}\n${indent}`));
786
+ return fixes;
787
+ }
788
+ // A comment on a line of its own before the `)` was not written against
789
+ // the last argument, so the option lands BEFORE it — after the trailing
790
+ // annotation, if the line carries one — and the comment keeps both its
791
+ // bytes and its neighbours: what it sat above, it still sits above.
792
+ const appendedText = appended
793
+ .map((argument) => `\n${body}${argument},`)
794
+ .join('');
795
+ const anchor = Math.max(separatorPresent ? beforeClose.range[1] : lastArgumentToken.range[1], lastSameLine ? lastSameLine.range[1] : 0);
796
+ if (separatorPresent || lastSameLine) {
797
+ if (!separatorPresent) {
798
+ fixes.push(fixer.insertTextAfter(lastArgumentToken, ','));
799
+ }
800
+ fixes.push(fixer.insertTextAfterRange([anchor, anchor], appendedText));
801
+ return fixes;
802
+ }
803
+ // With neither a comma nor a trailing comment, the separator and the
804
+ // option travel as one insertion, since both land at the same offset.
805
+ fixes.push(fixer.insertTextAfterRange([anchor, anchor], `,${appendedText}`));
746
806
  return fixes;
747
807
  }
748
808
  /**
@@ -800,10 +860,11 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
800
860
  `${indent}})`),
801
861
  ];
802
862
  }
803
- return [
804
- fixer.replaceText(callee.property, 'set'),
805
- ...appendArguments(fixer, node, [MERGE_OPTION], 'set'.length - callee.property.name.length),
806
- ];
863
+ const appended = appendArguments(fixer, node, [MERGE_OPTION], 'set'.length - callee.property.name.length);
864
+ if (!appended) {
865
+ return null;
866
+ }
867
+ return [fixer.replaceText(callee.property, 'set'), ...appended];
807
868
  }
808
869
  /**
809
870
  * The source the removal planner reads, with the comments inside a
@@ -970,17 +1031,23 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
970
1031
  const overlaps = ordered.some((rewrite, index) => index > 0 && rewrite.call.range[0] < ordered[index - 1].call.range[1]);
971
1032
  return overlaps ? null : rewrites;
972
1033
  }
973
- /** `updateDoc(ref, data)` → `setDoc(ref, data, { merge: true })`. */
1034
+ /**
1035
+ * `updateDoc(ref, data)` → `setDoc(ref, data, { merge: true })`, or `null`
1036
+ * where the argument list cannot be extended. No state is touched on the
1037
+ * way to that answer: the caller marks the call batched only once every
1038
+ * edit riding in the same fix is known to land, since a call marked by a
1039
+ * fix that never ships would make its own report decline too.
1040
+ */
974
1041
  function rewriteCall(fixer, rewrite) {
975
- batchedCalls.add(rewrite.call);
976
- return [
977
- fixer.replaceText(rewrite.identifier, SET_DOC),
978
- // `setDoc` takes the document data between the reference and the
979
- // options, so a call that passed no data gets an empty object to merge.
980
- ...appendArguments(fixer, rewrite.call, rewrite.call.arguments.length > 1
981
- ? [MERGE_OPTION]
982
- : [EMPTY_DATA, MERGE_OPTION], SET_DOC.length - rewrite.identifier.name.length),
983
- ];
1042
+ // `setDoc` takes the document data between the reference and the
1043
+ // options, so a call that passed no data gets an empty object to merge.
1044
+ const appended = appendArguments(fixer, rewrite.call, rewrite.call.arguments.length > 1
1045
+ ? [MERGE_OPTION]
1046
+ : [EMPTY_DATA, MERGE_OPTION], SET_DOC.length - rewrite.identifier.name.length);
1047
+ if (!appended) {
1048
+ return null;
1049
+ }
1050
+ return [fixer.replaceText(rewrite.identifier, SET_DOC), ...appended];
984
1051
  }
985
1052
  /**
986
1053
  * `updateDoc(ref, data)` becomes `setDoc(ref, data, { merge: true })`, which
@@ -1030,15 +1097,37 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
1030
1097
  if (!rewrites) {
1031
1098
  // A reference survives the pass, so the name stays bound and `setDoc` is
1032
1099
  // added alongside it. Only the first surviving violation carries the
1033
- // binding; the rest emit the call against it.
1100
+ // binding; the rest emit the call against it. The call rewrite is asked
1101
+ // for FIRST: a declined rewrite must leave the binding plan untouched,
1102
+ // or a later violation would trust an import this fix never emitted.
1103
+ const callFixes = rewriteCall(fixer, {
1104
+ identifier: callee,
1105
+ call: node,
1106
+ });
1107
+ if (!callFixes) {
1108
+ return null;
1109
+ }
1034
1110
  const fixes = [];
1035
1111
  if (!setDocVariable && !plannedSetDocBinding) {
1036
1112
  fixes.push(fixer.insertTextAfter(updateBinding.node, `, ${SET_DOC}`));
1037
1113
  plannedSetDocBinding = true;
1038
1114
  }
1039
- fixes.push(...rewriteCall(fixer, { identifier: callee, call: node }));
1115
+ batchedCalls.add(node);
1116
+ fixes.push(...callFixes);
1040
1117
  return fixes;
1041
1118
  }
1119
+ // One call whose tail cannot be extended declines the WHOLE batch, before
1120
+ // any flag records it as handled: a partial batch would retire an import
1121
+ // some call still reads, and a call marked batched by a fix that never
1122
+ // shipped would silently lose its own report's fix as well.
1123
+ const rewriteFixes = [];
1124
+ for (const rewrite of rewrites) {
1125
+ const callFixes = rewriteCall(fixer, rewrite);
1126
+ if (!callFixes) {
1127
+ return null;
1128
+ }
1129
+ rewriteFixes.push(...callFixes);
1130
+ }
1042
1131
  const fixes = [];
1043
1132
  if (setDocVariable) {
1044
1133
  // The name is already bound to firestore's own `setDoc`, so the entry
@@ -1062,8 +1151,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
1062
1151
  plannedSetDocBinding = true;
1063
1152
  }
1064
1153
  for (const rewrite of rewrites) {
1065
- fixes.push(...rewriteCall(fixer, rewrite));
1154
+ batchedCalls.add(rewrite.call);
1066
1155
  }
1156
+ fixes.push(...rewriteFixes);
1067
1157
  return fixes;
1068
1158
  }
1069
1159
  return {
@@ -519,8 +519,8 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
519
519
  // exactly as in the comment-free fix, never by the comments themselves.
520
520
  // The call can start mid-line, so the indentation of the line it opens
521
521
  // on is the only anchor the carried comments have.
522
- const startLine = sourceCode.lines[node.loc.start.line - 1] ?? '';
523
- const indent = /^[\t ]*/.exec(startLine)?.[0] ?? '';
522
+ const indentOf = (line) => /^[\t ]*/.exec(line)?.[0] ?? '';
523
+ const indent = indentOf(sourceCode.lines[node.loc.start.line - 1] ?? '');
524
524
  const text = sourceCode.getText();
525
525
  /**
526
526
  * A block comment's text carries the indentation of where it was
@@ -552,10 +552,11 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
552
552
  }),
553
553
  ].join('\n');
554
554
  };
555
- const toSegment = (comment) => ({
556
- text: reindented(comment, indent),
555
+ const segmentAt = (comment, toIndent) => ({
556
+ text: reindented(comment, toIndent),
557
557
  breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
558
558
  });
559
+ const toSegment = (comment) => segmentAt(comment, indent);
559
560
  // A stranded comment lies wholly on one side of the expression, since
560
561
  // a comment is a token and cannot straddle a node; keeping each on its
561
562
  // own side preserves what it annotates.
@@ -613,18 +614,43 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
613
614
  text: hoisted,
614
615
  });
615
616
  }
617
+ // A comment carried from behind the expression rides out past the
618
+ // punctuator that closes the statement the call stood in, where one
619
+ // is available to take over. Kept inside the replacement, the line
620
+ // break such a comment demands strands that punctuator on a line of
621
+ // its own — layout a formatter folds straight back, and the fold
622
+ // moves an `eslint-disable-next-line` written there onto a different
623
+ // subject, so the rule's own output decides which violations the
624
+ // next run enforces (#2138). Where no punctuator can be taken over,
625
+ // the comment stays inside the replacement and keeps its break.
626
+ const closingPunctuator = trailingComments.length > 0
627
+ ? (0, replacementSegments_1.absorbableClosingPunctuator)(sourceCode, node, trailingComments)
628
+ : null;
616
629
  const segments = [
617
630
  ...leadingComments
618
631
  .filter((comment) => !(0, replacementSegments_1.requiresOwnLine)(comment))
619
632
  .map(toSegment),
620
633
  { text: replacementText, breakAfter: false },
621
- ...trailingComments.map(toSegment),
634
+ ...(closingPunctuator ? [] : trailingComments.map(toSegment)),
622
635
  ];
623
636
  const body = (0, replacementSegments_1.joinSegmentBody)(segments, indent);
624
- const trailing = segments[segments.length - 1].breakAfter
625
- ? `\n${indent}`
626
- : '';
627
- edits.push({ range: node.range, text: `${body}${trailing}` });
637
+ if (!closingPunctuator) {
638
+ const trailing = segments[segments.length - 1].breakAfter
639
+ ? `\n${indent}`
640
+ : '';
641
+ edits.push({ range: node.range, text: `${body}${trailing}` });
642
+ }
643
+ else {
644
+ // A comment that lands out here annotates the statement rather
645
+ // than the expression, so it re-indents to the line the punctuator
646
+ // closes instead of to the line the call opened on.
647
+ const tailIndent = indentOf(sourceCode.lines[closingPunctuator.loc.end.line - 1] ?? '');
648
+ const tail = (0, replacementSegments_1.joinSegmentBody)(trailingComments.map((comment) => segmentAt(comment, tailIndent)), tailIndent);
649
+ edits.push({
650
+ range: [node.range[0], closingPunctuator.range[1]],
651
+ text: `${body}${closingPunctuator.value} ${tail}`,
652
+ });
653
+ }
628
654
  }
629
655
  }
630
656
  return {
@@ -1235,11 +1235,50 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
1235
1235
  ? landingOperator(node, sourceCode)
1236
1236
  : null;
1237
1237
  const leading = landing ? `\n${bodyIndent}` : '';
1238
- const body = (0, replacementSegments_1.joinSegmentBody)(segments, tailIndent);
1239
- const trailing = segments[segments.length - 1].breakAfter
1238
+ const range = replacementRange(node, sourceCode, landing);
1239
+ // A comment trailing the whole expression rides out past the
1240
+ // punctuator that closes the statement the chain stands in,
1241
+ // where one is available to take over. Kept inside the
1242
+ // replacement, the line break such a comment demands strands
1243
+ // that punctuator on a line of its own — layout a formatter
1244
+ // folds straight back, and the fold moves an
1245
+ // `eslint-disable-next-line` written on the comment's line
1246
+ // onto a different subject, so the pipeline's formatting order
1247
+ // decides which violations are enforced (#2139). The span this
1248
+ // fix replaces can end PAST the node, at a redundant paren the
1249
+ // widening claims, so the lookup anchors on the span's own
1250
+ // last token: asked of the node, it answers with that paren —
1251
+ // a token inside the span — instead of the punctuator behind
1252
+ // it.
1253
+ const spanEndToken = range[1] > node.range[1]
1254
+ ? sourceCode.getTokenAfter(node)
1255
+ : sourceCode.getLastToken(node);
1256
+ const closingPunctuator = comments.trailing.length > 0 && spanEndToken
1257
+ ? (0, replacementSegments_1.absorbableClosingPunctuator)(sourceCode, spanEndToken, comments.trailing)
1258
+ : null;
1259
+ const bodySegments = closingPunctuator
1260
+ ? segments.slice(0, segments.length - comments.trailing.length)
1261
+ : segments;
1262
+ const body = (0, replacementSegments_1.joinSegmentBody)(bodySegments, tailIndent);
1263
+ const trailing = !closingPunctuator && segments[segments.length - 1].breakAfter
1240
1264
  ? `\n${lineIndent}`
1241
1265
  : '';
1242
- const replacement = fixer.replaceTextRange(replacementRange(node, sourceCode, landing), `${leading}${body}${trailing}`);
1266
+ // A comment that lands out past the punctuator annotates the
1267
+ // statement rather than the expression, so it re-indents to
1268
+ // the line the punctuator closes instead of to the chain's own
1269
+ // depth.
1270
+ const closingLine = closingPunctuator
1271
+ ? sourceCode.lines[closingPunctuator.loc.end.line - 1] ?? ''
1272
+ : '';
1273
+ const carriedTail = closingPunctuator
1274
+ ? (0, replacementSegments_1.joinSegmentBody)(comments.trailing.map(toSegment), /^[\t ]*/.exec(closingLine)?.[0] ?? '')
1275
+ : '';
1276
+ const claimedRange = closingPunctuator
1277
+ ? [range[0], closingPunctuator.range[1]]
1278
+ : range;
1279
+ const replacement = fixer.replaceTextRange(claimedRange, closingPunctuator
1280
+ ? `${leading}${body}${closingPunctuator.value} ${carriedTail}`
1281
+ : `${leading}${body}${trailing}`);
1243
1282
  if (!keyword || hoisted.length === 0) {
1244
1283
  return replacement;
1245
1284
  }
@@ -1,4 +1,4 @@
1
- import { TSESTree } from '@typescript-eslint/utils';
1
+ import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
2
  /**
3
3
  * Building blocks for fixers that inline an expression in place of a larger
4
4
  * span (e.g. a `useMemo(...)` call) and must carry the comments stranded by
@@ -40,6 +40,36 @@ export declare function spansMultipleLines(comment: TSESTree.Comment): boolean;
40
40
  * and can keep such a comment inline.
41
41
  */
42
42
  export declare function requiresOwnLine(comment: TSESTree.Comment): boolean;
43
+ /**
44
+ * The punctuator a comment carried from behind the expression may move past.
45
+ *
46
+ * Such a comment annotates the statement the node stood in, and prettier
47
+ * prints it AFTER the token that closes that statement — `const x = 1; /* c
48
+ * *\/`, never `const x = 1 /* c *\/;`. Leaving it inside the statement is
49
+ * layout prettier rewrites, so the fixed file fails `prettier --check`
50
+ * (#2079). Leaving it there also strands the punctuator on a line of its own
51
+ * behind a line-bound comment, which retargets an `eslint-disable-next-line`
52
+ * onto that stray line the moment prettier folds it back (#2138).
53
+ *
54
+ * A comma is a weaker claim and takes only the comments that need a line of
55
+ * their own. Measured against this repo's prettier: a line comment on a list
56
+ * element is printed after the comma, while a block comment on one is
57
+ * printed before it — the opposite of what a semicolon gets. Moving a block
58
+ * comment past a comma would trade one layout prettier rewrites for another.
59
+ *
60
+ * Only a punctuator with nothing but line ending behind it may be taken
61
+ * over: a line-bound comment landing after it would otherwise swallow
62
+ * whatever shared that line. Asking for the next token INCLUDING comments
63
+ * also keeps a comment the fixer does not own from being stepped over.
64
+ *
65
+ * The anchor is whatever the replaced SPAN ends at. Where that span is
66
+ * exactly the node, the node itself serves; where a widening claims tokens
67
+ * past the node — redundant parentheses the rewrite discards — the anchor
68
+ * must be the span's own last token, because asked of the node the lookup
69
+ * answers with a token INSIDE the span instead of the punctuator behind it
70
+ * (#2139).
71
+ */
72
+ export declare function absorbableClosingPunctuator(sourceCode: Readonly<TSESLint.SourceCode>, anchor: TSESTree.Node | TSESTree.Token, trailingComments: readonly TSESTree.Comment[]): TSESTree.Token | null;
43
73
  export type ReplacementSegment = {
44
74
  text: string;
45
75
  breakAfter: boolean;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.joinSegments = exports.joinSegmentBody = exports.requiresOwnLine = exports.spansMultipleLines = exports.requiresLineBreakAfter = void 0;
3
+ exports.joinSegments = exports.joinSegmentBody = exports.absorbableClosingPunctuator = exports.requiresOwnLine = exports.spansMultipleLines = exports.requiresLineBreakAfter = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const disableDirectives_1 = require("./disableDirectives");
6
6
  /**
@@ -56,6 +56,49 @@ function requiresOwnLine(comment) {
56
56
  return requiresLineBreakAfter(comment) || spansMultipleLines(comment);
57
57
  }
58
58
  exports.requiresOwnLine = requiresOwnLine;
59
+ /**
60
+ * The punctuator a comment carried from behind the expression may move past.
61
+ *
62
+ * Such a comment annotates the statement the node stood in, and prettier
63
+ * prints it AFTER the token that closes that statement — `const x = 1; /* c
64
+ * *\/`, never `const x = 1 /* c *\/;`. Leaving it inside the statement is
65
+ * layout prettier rewrites, so the fixed file fails `prettier --check`
66
+ * (#2079). Leaving it there also strands the punctuator on a line of its own
67
+ * behind a line-bound comment, which retargets an `eslint-disable-next-line`
68
+ * onto that stray line the moment prettier folds it back (#2138).
69
+ *
70
+ * A comma is a weaker claim and takes only the comments that need a line of
71
+ * their own. Measured against this repo's prettier: a line comment on a list
72
+ * element is printed after the comma, while a block comment on one is
73
+ * printed before it — the opposite of what a semicolon gets. Moving a block
74
+ * comment past a comma would trade one layout prettier rewrites for another.
75
+ *
76
+ * Only a punctuator with nothing but line ending behind it may be taken
77
+ * over: a line-bound comment landing after it would otherwise swallow
78
+ * whatever shared that line. Asking for the next token INCLUDING comments
79
+ * also keeps a comment the fixer does not own from being stepped over.
80
+ *
81
+ * The anchor is whatever the replaced SPAN ends at. Where that span is
82
+ * exactly the node, the node itself serves; where a widening claims tokens
83
+ * past the node — redundant parentheses the rewrite discards — the anchor
84
+ * must be the span's own last token, because asked of the node the lookup
85
+ * answers with a token INSIDE the span instead of the punctuator behind it
86
+ * (#2139).
87
+ */
88
+ function absorbableClosingPunctuator(sourceCode, anchor, trailingComments) {
89
+ const next = sourceCode.getTokenAfter(anchor, { includeComments: true });
90
+ if (!next ||
91
+ next.type !== utils_1.AST_TOKEN_TYPES.Punctuator ||
92
+ (next.value !== ';' && next.value !== ',')) {
93
+ return null;
94
+ }
95
+ if (next.value === ',' && !trailingComments.every(requiresLineBreakAfter)) {
96
+ return null;
97
+ }
98
+ const line = sourceCode.lines[next.loc.end.line - 1] ?? '';
99
+ return line.slice(next.loc.end.column).trim() === '' ? next : null;
100
+ }
101
+ exports.absorbableClosingPunctuator = absorbableClosingPunctuator;
59
102
  /**
60
103
  * Joins the inlined expression and its carried comments into one run of text,
61
104
  * keeping each comment on the side of the expression it was written on and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.173",
3
+ "version": "1.20.174",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "version": "1.20.174",
4
+ "date": "2026-08-26T04:47:21.242Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-firestore-set-merge",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2140
11
+ ],
12
+ "summary": "emit the appended option's separator before a trailing comment (closes #2140)"
13
+ },
14
+ {
15
+ "name": "no-usememo-for-pass-by-value",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 2138
19
+ ],
20
+ "summary": "carry the trailing comment past the statement terminator (closes #2138)"
21
+ },
22
+ {
23
+ "name": "prefer-nullish-coalescing-boolean-props",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 2139
27
+ ],
28
+ "summary": "carry the trailing comment past the statement terminator (closes #2139)"
29
+ }
30
+ ]
31
+ },
2
32
  {
3
33
  "version": "1.20.173",
4
34
  "date": "2026-08-25T23:22:38.601Z",