@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 +1 -1
- package/lib/rules/enforce-firestore-set-merge.js +119 -29
- package/lib/rules/no-usememo-for-pass-by-value.js +35 -9
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +42 -3
- package/lib/utils/replacementSegments.d.ts +31 -1
- package/lib/utils/replacementSegments.js +44 -1
- package/package.json +1 -1
- package/release-manifest.json +30 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
744
|
-
.
|
|
745
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
523
|
-
const indent =
|
|
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
|
|
556
|
-
text: reindented(comment,
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
1239
|
-
|
|
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
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|