@blumintinc/eslint-plugin-blumint 1.20.173 → 1.20.175
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 +238 -39
- package/lib/rules/no-usememo-for-pass-by-value.js +35 -9
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +69 -11
- package/lib/utils/replacementSegments.d.ts +31 -1
- package/lib/utils/replacementSegments.js +44 -1
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -647,9 +647,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
647
647
|
* inline there emits text the formatter immediately re-breaks, so the fix
|
|
648
648
|
* is never a fixed point of the consumer's own formatting pass (#2097).
|
|
649
649
|
*
|
|
650
|
-
* A list written across lines answers the first half outright
|
|
651
|
-
*
|
|
652
|
-
* the
|
|
650
|
+
* A list written across lines answers the first half outright and is
|
|
651
|
+
* handled before this is asked: the caller keeps it broken, except for
|
|
652
|
+
* the block-comment tail {@link flattenedListFixes} claims (#2142).
|
|
653
653
|
*
|
|
654
654
|
* The width half is answered only where the formatter's own answer is
|
|
655
655
|
* modelled end to end. A trailing options object gets hugged against the
|
|
@@ -683,16 +683,93 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
683
683
|
const widest = Math.max(...spans.map((span) => span.end - span.start), ...appended.map((argument) => argument.length));
|
|
684
684
|
return body + widest + ','.length <= PRINT_WIDTH;
|
|
685
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* The width the call prints at with its argument list riding one line:
|
|
688
|
+
* everything on the opening line through the parenthesis, each span
|
|
689
|
+
* joined by `, `, the tail comments a written comma strands outside the
|
|
690
|
+
* last span, the appended arguments, and whatever follows the closing
|
|
691
|
+
* parenthesis on its own line.
|
|
692
|
+
*/
|
|
693
|
+
function flatListWidth(layout, appended, nameDelta, gapComments) {
|
|
694
|
+
const { openParen, closeParen, spans } = layout;
|
|
695
|
+
const lastSpanEnd = spans[spans.length - 1].end;
|
|
696
|
+
const suffix = (sourceCode.lines[closeParen.loc.start.line - 1] ?? '')
|
|
697
|
+
.slice(closeParen.loc.start.column)
|
|
698
|
+
.trimEnd();
|
|
699
|
+
return (openParen.loc.end.column +
|
|
700
|
+
nameDelta +
|
|
701
|
+
spans.reduce((width, span) => width + (span.end - span.start), 0) +
|
|
702
|
+
(spans.length - 1) * ', '.length +
|
|
703
|
+
gapComments
|
|
704
|
+
.filter((comment) => comment.range[0] >= lastSpanEnd)
|
|
705
|
+
.reduce((width, comment) => width + ' '.length + (comment.range[1] - comment.range[0]), 0) +
|
|
706
|
+
appended.reduce((width, argument) => width + ', '.length + argument.length, 0) +
|
|
707
|
+
suffix.length);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The edits that print an authored-broken list flat, with `appended` on
|
|
711
|
+
* its tail — or `null` where the flat layout is out of reach or out of
|
|
712
|
+
* scope.
|
|
713
|
+
*
|
|
714
|
+
* A list written across lines usually stays broken: a multi-line argument
|
|
715
|
+
* cannot be inlined without rewriting its text, and a line comment pins
|
|
716
|
+
* its break outright. A break held up by nothing but a BLOCK comment
|
|
717
|
+
* trailing the last argument is neither — a block comment is no line
|
|
718
|
+
* terminator, so the consumer's formatter collapses the whole call as
|
|
719
|
+
* soon as it fits the print width, and no broken emission is a fixed
|
|
720
|
+
* point of its formatting pass there (#2142). Only that annotated tail
|
|
721
|
+
* claims the flat layout; elsewhere the author's breaks are kept — a
|
|
722
|
+
* choice the formatter may fold, but one this fix did not create.
|
|
723
|
+
*/
|
|
724
|
+
function flattenedListFixes(fixer, node, layout, appended, nameDelta, beforeClose, gapComments, ownLineComments) {
|
|
725
|
+
const { openParen, closeParen, spans } = layout;
|
|
726
|
+
if (gapComments.length === 0 ||
|
|
727
|
+
ownLineComments.length > 0 ||
|
|
728
|
+
gapComments.some(replacementSegments_1.requiresOwnLine) ||
|
|
729
|
+
calleeBreaksFirst(node.callee) ||
|
|
730
|
+
spans.some((span) => sourceCode.text.slice(span.start, span.end).includes('\n')) ||
|
|
731
|
+
flatListWidth(layout, appended, nameDelta, gapComments) > PRINT_WIDTH) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const fixes = [
|
|
735
|
+
fixer.replaceTextRange([openParen.range[1], spans[0].start], ''),
|
|
736
|
+
];
|
|
737
|
+
for (let index = 1; index < spans.length; index++) {
|
|
738
|
+
fixes.push(fixer.replaceTextRange([spans[index - 1].end, spans[index].start], ', '));
|
|
739
|
+
}
|
|
740
|
+
const lastSpanEnd = spans[spans.length - 1].end;
|
|
741
|
+
const flatTail = appended.map((argument) => `, ${argument}`).join('');
|
|
742
|
+
if (beforeClose.value !== ',') {
|
|
743
|
+
// With no written comma the tail comments sit inside the last span,
|
|
744
|
+
// so everything between the span and the parenthesis is whitespace.
|
|
745
|
+
fixes.push(fixer.replaceTextRange([lastSpanEnd, closeParen.range[0]], flatTail));
|
|
746
|
+
return fixes;
|
|
747
|
+
}
|
|
748
|
+
const pastComma = gapComments.filter((comment) => comment.range[0] >= beforeClose.range[1]);
|
|
749
|
+
if (pastComma.length === 0) {
|
|
750
|
+
// The written comma already trails the annotation; it is the
|
|
751
|
+
// separator the appended arguments ride on.
|
|
752
|
+
fixes.push(fixer.replaceTextRange([beforeClose.range[1], closeParen.range[0]], ` ${appended.join(', ')}`));
|
|
753
|
+
return fixes;
|
|
754
|
+
}
|
|
755
|
+
// The written comma moves past the comments it precedes: prettier
|
|
756
|
+
// prints a block comment on a list element BEFORE the separator.
|
|
757
|
+
fixes.push(fixer.removeRange(beforeClose.range));
|
|
758
|
+
fixes.push(fixer.replaceTextRange([pastComma[pastComma.length - 1].range[1], closeParen.range[0]], flatTail));
|
|
759
|
+
return fixes;
|
|
760
|
+
}
|
|
686
761
|
/**
|
|
687
762
|
* 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
|
|
763
|
+
* out the way the consumer's formatter prints the result — or `null` for
|
|
764
|
+
* the one tail no edit can extend without retargeting a directive.
|
|
689
765
|
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
766
|
+
* No layout re-emits an argument from its text. The broken one rewrites
|
|
767
|
+
* the SEPARATORS between the arguments and shifts the indentation of the
|
|
768
|
+
* ones that span lines, and the flat one rewrites the separators alone,
|
|
769
|
+
* so everything between them — comments included, and a dropped
|
|
770
|
+
* `eslint-disable` silently re-enables the rule it was suppressing
|
|
771
|
+
* (#1877) — stays where it was written, attached to the argument it
|
|
772
|
+
* belongs to.
|
|
696
773
|
*
|
|
697
774
|
* `nameDelta` is how much the caller's own rename widens the call, since
|
|
698
775
|
* the two edits land on the same line and the width answer is about the
|
|
@@ -704,20 +781,53 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
704
781
|
fixer.insertTextAfter(args[args.length - 1], appended.map((argument) => `, ${argument}`).join('')),
|
|
705
782
|
];
|
|
706
783
|
const layout = callLayout(node);
|
|
707
|
-
if (!layout
|
|
784
|
+
if (!layout) {
|
|
708
785
|
return inline();
|
|
709
786
|
}
|
|
710
787
|
const { openParen, closeParen, spans } = layout;
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
788
|
+
const listSpansLines = sourceCode.text
|
|
789
|
+
.slice(openParen.range[1], closeParen.range[0])
|
|
790
|
+
.includes('\n');
|
|
791
|
+
if (!listSpansLines &&
|
|
792
|
+
!requiresBrokenList(node, layout, appended, nameDelta)) {
|
|
793
|
+
return inline();
|
|
794
|
+
}
|
|
795
|
+
// Between the last argument's own last token and the closing parenthesis
|
|
796
|
+
// sit the trailing comma, if one was written, and any comment. The tail
|
|
797
|
+
// SPAN absorbs such a comment whenever no comma follows it, so the span's
|
|
798
|
+
// end is not a safe place to write a separator: a `,` emitted after a
|
|
799
|
+
// line comment is swallowed into the comment's text and the call no
|
|
800
|
+
// longer parses (#2140). Both boundaries are therefore taken from the
|
|
801
|
+
// TOKEN stream, where a comment can never be the answer.
|
|
802
|
+
const beforeClose = sourceCode.getTokenBefore(closeParen);
|
|
803
|
+
const lastArgumentToken = beforeClose?.value === ','
|
|
804
|
+
? sourceCode.getTokenBefore(beforeClose)
|
|
805
|
+
: beforeClose;
|
|
806
|
+
if (!beforeClose || !lastArgumentToken) {
|
|
719
807
|
return inline();
|
|
720
808
|
}
|
|
809
|
+
const gapComments = sourceCode
|
|
810
|
+
.getCommentsInside(node)
|
|
811
|
+
.filter((comment) => comment.range[0] >= lastArgumentToken.range[1]);
|
|
812
|
+
const sameLineComments = gapComments.filter((comment) => comment.loc.start.line === lastArgumentToken.loc.end.line);
|
|
813
|
+
const ownLineComments = gapComments.filter((comment) => comment.loc.start.line !== lastArgumentToken.loc.end.line);
|
|
814
|
+
// A directive trailing the argument's line means the line that FOLLOWS
|
|
815
|
+
// it: emitting anything there hands `{ merge: true }` the suppression
|
|
816
|
+
// that was written for the closing line, and re-exposes whatever it was
|
|
817
|
+
// suppressing. No layout preserves both the option's position and the
|
|
818
|
+
// directive's subject, so the fix is withheld and the report left to the
|
|
819
|
+
// developer, who restructures the call with the directive in view
|
|
820
|
+
// (#1877). A directive on a line of its OWN is safe: the option lands
|
|
821
|
+
// BEFORE it, so everything the directive is adjacent to stays adjacent.
|
|
822
|
+
if (sameLineComments.some(isPositionalDirective)) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
if (listSpansLines) {
|
|
826
|
+
const flattened = flattenedListFixes(fixer, node, layout, appended, nameDelta, beforeClose, gapComments, ownLineComments);
|
|
827
|
+
if (flattened) {
|
|
828
|
+
return flattened;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
721
831
|
// A broken list is indented one step past the line its parenthesis opens
|
|
722
832
|
// on, whatever depth that line sits at, and closes at that line's own
|
|
723
833
|
// column. A constant indent is right for exactly one call site.
|
|
@@ -740,9 +850,68 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
740
850
|
fixes.push(fixer.replaceText(argument, relocated));
|
|
741
851
|
}
|
|
742
852
|
}
|
|
743
|
-
|
|
744
|
-
.
|
|
745
|
-
|
|
853
|
+
if (gapComments.length === 0) {
|
|
854
|
+
fixes.push(fixer.replaceTextRange([lastArgumentToken.range[1], closeParen.range[0]], `${appended
|
|
855
|
+
.map((argument) => `,\n${body}${argument}`)
|
|
856
|
+
.join('')},\n${indent}`));
|
|
857
|
+
return fixes;
|
|
858
|
+
}
|
|
859
|
+
const separatorPresent = beforeClose.value === ',';
|
|
860
|
+
const lastSameLine = sameLineComments[sameLineComments.length - 1];
|
|
861
|
+
// WHERE the separator sits among the trailing comments is the
|
|
862
|
+
// formatter's call, not a constant: prettier prints a line comment on a
|
|
863
|
+
// list element after the comma and a block comment before it (#2142).
|
|
864
|
+
// The comma therefore lands past the leading run of block comments — at
|
|
865
|
+
// the argument's own last token when that run is empty — and a comma
|
|
866
|
+
// the author wrote on the wrong side of the run is moved rather than
|
|
867
|
+
// doubled. Everything else in the annotation is left byte for byte as
|
|
868
|
+
// written.
|
|
869
|
+
let blockRunEnd = lastArgumentToken.range[1];
|
|
870
|
+
for (const comment of sameLineComments) {
|
|
871
|
+
if ((0, replacementSegments_1.requiresLineBreakAfter)(comment)) {
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
blockRunEnd = comment.range[1];
|
|
875
|
+
}
|
|
876
|
+
const commaMisplaced = separatorPresent && beforeClose.range[1] < blockRunEnd;
|
|
877
|
+
const needsComma = !separatorPresent || commaMisplaced;
|
|
878
|
+
if (commaMisplaced) {
|
|
879
|
+
fixes.push(fixer.removeRange(beforeClose.range));
|
|
880
|
+
}
|
|
881
|
+
if (ownLineComments.length === 0) {
|
|
882
|
+
// The comment trails the argument it annotates, so it keeps that line
|
|
883
|
+
// and the option opens the next one.
|
|
884
|
+
const annotationEnd = Math.max(lastSameLine.range[1], beforeClose.range[1]);
|
|
885
|
+
if (needsComma && blockRunEnd < annotationEnd) {
|
|
886
|
+
fixes.push(fixer.insertTextAfterRange([blockRunEnd, blockRunEnd], ','));
|
|
887
|
+
}
|
|
888
|
+
fixes.push(fixer.replaceTextRange([annotationEnd, closeParen.range[0]], `${needsComma && blockRunEnd >= annotationEnd ? ',' : ''}${appended
|
|
889
|
+
.map((argument) => `\n${body}${argument},`)
|
|
890
|
+
.join('')}\n${indent}`));
|
|
891
|
+
return fixes;
|
|
892
|
+
}
|
|
893
|
+
// A comment on a line of its own before the `)` was not written against
|
|
894
|
+
// the last argument, so the option lands BEFORE it — after the trailing
|
|
895
|
+
// annotation, if the line carries one — and the comment keeps both its
|
|
896
|
+
// bytes and its neighbours: what it sat above, it still sits above.
|
|
897
|
+
const appendedText = appended
|
|
898
|
+
.map((argument) => `\n${body}${argument},`)
|
|
899
|
+
.join('');
|
|
900
|
+
const anchor = Math.max(separatorPresent && !commaMisplaced
|
|
901
|
+
? beforeClose.range[1]
|
|
902
|
+
: lastArgumentToken.range[1], lastSameLine ? lastSameLine.range[1] : 0);
|
|
903
|
+
if (!needsComma) {
|
|
904
|
+
fixes.push(fixer.insertTextAfterRange([anchor, anchor], appendedText));
|
|
905
|
+
return fixes;
|
|
906
|
+
}
|
|
907
|
+
if (blockRunEnd < anchor) {
|
|
908
|
+
fixes.push(fixer.insertTextAfterRange([blockRunEnd, blockRunEnd], ','));
|
|
909
|
+
fixes.push(fixer.insertTextAfterRange([anchor, anchor], appendedText));
|
|
910
|
+
return fixes;
|
|
911
|
+
}
|
|
912
|
+
// With the separator due at the annotation's own end, it and the option
|
|
913
|
+
// travel as one insertion, since both land at the same offset.
|
|
914
|
+
fixes.push(fixer.insertTextAfterRange([anchor, anchor], `,${appendedText}`));
|
|
746
915
|
return fixes;
|
|
747
916
|
}
|
|
748
917
|
/**
|
|
@@ -800,10 +969,11 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
800
969
|
`${indent}})`),
|
|
801
970
|
];
|
|
802
971
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
972
|
+
const appended = appendArguments(fixer, node, [MERGE_OPTION], 'set'.length - callee.property.name.length);
|
|
973
|
+
if (!appended) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return [fixer.replaceText(callee.property, 'set'), ...appended];
|
|
807
977
|
}
|
|
808
978
|
/**
|
|
809
979
|
* The source the removal planner reads, with the comments inside a
|
|
@@ -970,17 +1140,23 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
970
1140
|
const overlaps = ordered.some((rewrite, index) => index > 0 && rewrite.call.range[0] < ordered[index - 1].call.range[1]);
|
|
971
1141
|
return overlaps ? null : rewrites;
|
|
972
1142
|
}
|
|
973
|
-
/**
|
|
1143
|
+
/**
|
|
1144
|
+
* `updateDoc(ref, data)` → `setDoc(ref, data, { merge: true })`, or `null`
|
|
1145
|
+
* where the argument list cannot be extended. No state is touched on the
|
|
1146
|
+
* way to that answer: the caller marks the call batched only once every
|
|
1147
|
+
* edit riding in the same fix is known to land, since a call marked by a
|
|
1148
|
+
* fix that never ships would make its own report decline too.
|
|
1149
|
+
*/
|
|
974
1150
|
function rewriteCall(fixer, rewrite) {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
];
|
|
1151
|
+
// `setDoc` takes the document data between the reference and the
|
|
1152
|
+
// options, so a call that passed no data gets an empty object to merge.
|
|
1153
|
+
const appended = appendArguments(fixer, rewrite.call, rewrite.call.arguments.length > 1
|
|
1154
|
+
? [MERGE_OPTION]
|
|
1155
|
+
: [EMPTY_DATA, MERGE_OPTION], SET_DOC.length - rewrite.identifier.name.length);
|
|
1156
|
+
if (!appended) {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
return [fixer.replaceText(rewrite.identifier, SET_DOC), ...appended];
|
|
984
1160
|
}
|
|
985
1161
|
/**
|
|
986
1162
|
* `updateDoc(ref, data)` becomes `setDoc(ref, data, { merge: true })`, which
|
|
@@ -1030,15 +1206,37 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
1030
1206
|
if (!rewrites) {
|
|
1031
1207
|
// A reference survives the pass, so the name stays bound and `setDoc` is
|
|
1032
1208
|
// added alongside it. Only the first surviving violation carries the
|
|
1033
|
-
// binding; the rest emit the call against it.
|
|
1209
|
+
// binding; the rest emit the call against it. The call rewrite is asked
|
|
1210
|
+
// for FIRST: a declined rewrite must leave the binding plan untouched,
|
|
1211
|
+
// or a later violation would trust an import this fix never emitted.
|
|
1212
|
+
const callFixes = rewriteCall(fixer, {
|
|
1213
|
+
identifier: callee,
|
|
1214
|
+
call: node,
|
|
1215
|
+
});
|
|
1216
|
+
if (!callFixes) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1034
1219
|
const fixes = [];
|
|
1035
1220
|
if (!setDocVariable && !plannedSetDocBinding) {
|
|
1036
1221
|
fixes.push(fixer.insertTextAfter(updateBinding.node, `, ${SET_DOC}`));
|
|
1037
1222
|
plannedSetDocBinding = true;
|
|
1038
1223
|
}
|
|
1039
|
-
|
|
1224
|
+
batchedCalls.add(node);
|
|
1225
|
+
fixes.push(...callFixes);
|
|
1040
1226
|
return fixes;
|
|
1041
1227
|
}
|
|
1228
|
+
// One call whose tail cannot be extended declines the WHOLE batch, before
|
|
1229
|
+
// any flag records it as handled: a partial batch would retire an import
|
|
1230
|
+
// some call still reads, and a call marked batched by a fix that never
|
|
1231
|
+
// shipped would silently lose its own report's fix as well.
|
|
1232
|
+
const rewriteFixes = [];
|
|
1233
|
+
for (const rewrite of rewrites) {
|
|
1234
|
+
const callFixes = rewriteCall(fixer, rewrite);
|
|
1235
|
+
if (!callFixes) {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
rewriteFixes.push(...callFixes);
|
|
1239
|
+
}
|
|
1042
1240
|
const fixes = [];
|
|
1043
1241
|
if (setDocVariable) {
|
|
1044
1242
|
// The name is already bound to firestore's own `setDoc`, so the entry
|
|
@@ -1062,8 +1260,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
1062
1260
|
plannedSetDocBinding = true;
|
|
1063
1261
|
}
|
|
1064
1262
|
for (const rewrite of rewrites) {
|
|
1065
|
-
|
|
1263
|
+
batchedCalls.add(rewrite.call);
|
|
1066
1264
|
}
|
|
1265
|
+
fixes.push(...rewriteFixes);
|
|
1067
1266
|
return fixes;
|
|
1068
1267
|
}
|
|
1069
1268
|
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 {
|
|
@@ -883,14 +883,19 @@ function chainComments(node, sourceCode, operands) {
|
|
|
883
883
|
* inside an operand's own brackets — a line comment in an object literal passed
|
|
884
884
|
* to a call — belongs to that operand's layout and leaves the chain's own layout
|
|
885
885
|
* alone, which is how prettier prints it.
|
|
886
|
+
*
|
|
887
|
+
* `absorbed` holds the comments the rewrite carries out past the statement's
|
|
888
|
+
* closing punctuator: those sit in no gap of the emitted chain, so counting one
|
|
889
|
+
* as a break would split a group that ends up holding no comment at all
|
|
890
|
+
* (#2141).
|
|
886
891
|
*/
|
|
887
|
-
function chainBreaksLine(node, sourceCode) {
|
|
892
|
+
function chainBreaksLine(node, sourceCode, absorbed) {
|
|
888
893
|
if (node.type !== utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
889
894
|
return false;
|
|
890
895
|
}
|
|
891
|
-
return (strandedComments(node, sourceCode).some(replacementSegments_1.requiresLineBreakAfter) ||
|
|
892
|
-
chainBreaksLine(node.left, sourceCode) ||
|
|
893
|
-
chainBreaksLine(node.right, sourceCode));
|
|
896
|
+
return (strandedComments(node, sourceCode).some((comment) => !absorbed.has(comment) && (0, replacementSegments_1.requiresLineBreakAfter)(comment)) ||
|
|
897
|
+
chainBreaksLine(node.left, sourceCode, absorbed) ||
|
|
898
|
+
chainBreaksLine(node.right, sourceCode, absorbed));
|
|
894
899
|
}
|
|
895
900
|
/**
|
|
896
901
|
* Whether the chain a link belongs to breaks the line, asked of the whole chain
|
|
@@ -902,12 +907,16 @@ function chainBreaksLine(node, sourceCode) {
|
|
|
902
907
|
* broken packs two operands onto a line the formatter then splits (#2106).
|
|
903
908
|
*
|
|
904
909
|
* A comment trailing the chain, or leading it, is outside every gap: prettier
|
|
905
|
-
* prints it on the line the chain already occupies and moves no operand.
|
|
910
|
+
* prints it on the line the chain already occupies and moves no operand. The
|
|
911
|
+
* same goes for a comment in `absorbed` — one the fix carries out past the
|
|
912
|
+
* statement's closing punctuator: wherever the INPUT held it, the emission
|
|
913
|
+
* holds it behind the statement, outside every gap (#2141).
|
|
906
914
|
*/
|
|
907
|
-
function chainBreaksAtAGap(chain, sourceCode) {
|
|
915
|
+
function chainBreaksAtAGap(chain, sourceCode, absorbed) {
|
|
908
916
|
const operands = chainOperands(chain);
|
|
909
917
|
const { gaps } = chainComments(chain, sourceCode, operands);
|
|
910
|
-
return (gaps.some((gap) => [...gap.beforeOperator, ...gap.afterOperator].some(
|
|
918
|
+
return (gaps.some((gap) => [...gap.beforeOperator, ...gap.afterOperator].some((comment) => !absorbed.has(comment) && (0, replacementSegments_1.requiresLineBreakAfter)(comment))) ||
|
|
919
|
+
operands.some((operand) => chainBreaksLine(operand, sourceCode, absorbed)));
|
|
911
920
|
}
|
|
912
921
|
/**
|
|
913
922
|
* The depths the rebuilt expression lands at.
|
|
@@ -1196,6 +1205,36 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1196
1205
|
});
|
|
1197
1206
|
});
|
|
1198
1207
|
segments.push(...comments.trailing.map(toSegment));
|
|
1208
|
+
// A comment trailing the whole expression rides out past the
|
|
1209
|
+
// punctuator that closes the statement the chain stands in,
|
|
1210
|
+
// where one is available to take over. Kept inside the
|
|
1211
|
+
// replacement, the line break such a comment demands strands
|
|
1212
|
+
// that punctuator on a line of its own — layout a formatter
|
|
1213
|
+
// folds straight back, and the fold moves an
|
|
1214
|
+
// `eslint-disable-next-line` written on the comment's line
|
|
1215
|
+
// onto a different subject, so the pipeline's formatting order
|
|
1216
|
+
// decides which violations are enforced (#2139). The span this
|
|
1217
|
+
// fix replaces can end PAST the node, at a redundant paren the
|
|
1218
|
+
// widening claims, so the lookup anchors on that paren where
|
|
1219
|
+
// the widening applies: asked of the node, it answers with the
|
|
1220
|
+
// paren — a token inside the span — instead of the punctuator
|
|
1221
|
+
// behind it. The landing widening reaches BACKWARD from the
|
|
1222
|
+
// node and never past its end, so everywhere else the span
|
|
1223
|
+
// ends at the node's own last token. The parenthesized
|
|
1224
|
+
// (selfParens) replacement keeps every comment inside the
|
|
1225
|
+
// emitted parens, where no punctuator can be stranded.
|
|
1226
|
+
const spanEndToken = redundantParens(node, sourceCode)
|
|
1227
|
+
? sourceCode.getTokenAfter(node)
|
|
1228
|
+
: sourceCode.getLastToken(node);
|
|
1229
|
+
const closingPunctuator = !selfParens && comments.trailing.length > 0 && spanEndToken
|
|
1230
|
+
? (0, replacementSegments_1.absorbableClosingPunctuator)(sourceCode, spanEndToken, comments.trailing)
|
|
1231
|
+
: null;
|
|
1232
|
+
// The absorption is decided BEFORE the break decision because
|
|
1233
|
+
// a comment carried past the punctuator sits in no gap of the
|
|
1234
|
+
// emitted chain: counted as a break it would put every operand
|
|
1235
|
+
// on a line of its own for a comment that no longer sits
|
|
1236
|
+
// between any of them (#2141).
|
|
1237
|
+
const absorbed = new Set(closingPunctuator ? comments.trailing : []);
|
|
1199
1238
|
// Prettier prints a logical chain as ONE group: once anything
|
|
1200
1239
|
// inside it breaks the line, EVERY operand takes a line of its
|
|
1201
1240
|
// own. So a chain broken by a comment anywhere gets a break in
|
|
@@ -1204,7 +1243,7 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1204
1243
|
// The break rides on the last thing in the gap so a carried
|
|
1205
1244
|
// comment keeps the line it was written on, and a gap already
|
|
1206
1245
|
// holding a break needs no second separator.
|
|
1207
|
-
const chainBreaks = chainBreaksAtAGap(chainRootOf(node), sourceCode);
|
|
1246
|
+
const chainBreaks = chainBreaksAtAGap(chainRootOf(node), sourceCode, absorbed);
|
|
1208
1247
|
if (chainBreaks) {
|
|
1209
1248
|
for (const { start, end } of gapSpans) {
|
|
1210
1249
|
const separated = segments
|
|
@@ -1235,11 +1274,30 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
1235
1274
|
? landingOperator(node, sourceCode)
|
|
1236
1275
|
: null;
|
|
1237
1276
|
const leading = landing ? `\n${bodyIndent}` : '';
|
|
1238
|
-
const
|
|
1239
|
-
const
|
|
1277
|
+
const range = replacementRange(node, sourceCode, landing);
|
|
1278
|
+
const bodySegments = closingPunctuator
|
|
1279
|
+
? segments.slice(0, segments.length - comments.trailing.length)
|
|
1280
|
+
: segments;
|
|
1281
|
+
const body = (0, replacementSegments_1.joinSegmentBody)(bodySegments, tailIndent);
|
|
1282
|
+
const trailing = !closingPunctuator && segments[segments.length - 1].breakAfter
|
|
1240
1283
|
? `\n${lineIndent}`
|
|
1241
1284
|
: '';
|
|
1242
|
-
|
|
1285
|
+
// A comment that lands out past the punctuator annotates the
|
|
1286
|
+
// statement rather than the expression, so it re-indents to
|
|
1287
|
+
// the line the punctuator closes instead of to the chain's own
|
|
1288
|
+
// depth.
|
|
1289
|
+
const closingLine = closingPunctuator
|
|
1290
|
+
? sourceCode.lines[closingPunctuator.loc.end.line - 1] ?? ''
|
|
1291
|
+
: '';
|
|
1292
|
+
const carriedTail = closingPunctuator
|
|
1293
|
+
? (0, replacementSegments_1.joinSegmentBody)(comments.trailing.map(toSegment), /^[\t ]*/.exec(closingLine)?.[0] ?? '')
|
|
1294
|
+
: '';
|
|
1295
|
+
const claimedRange = closingPunctuator
|
|
1296
|
+
? [range[0], closingPunctuator.range[1]]
|
|
1297
|
+
: range;
|
|
1298
|
+
const replacement = fixer.replaceTextRange(claimedRange, closingPunctuator
|
|
1299
|
+
? `${leading}${body}${closingPunctuator.value} ${carriedTail}`
|
|
1300
|
+
: `${leading}${body}${trailing}`);
|
|
1243
1301
|
if (!keyword || hoisted.length === 0) {
|
|
1244
1302
|
return replacement;
|
|
1245
1303
|
}
|
|
@@ -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,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.175",
|
|
4
|
+
"date": "2026-08-26T06:19:14.998Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-set-merge",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2142
|
|
11
|
+
],
|
|
12
|
+
"summary": "place a block comment before the separator and fold a list that fits (closes #2142)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-nullish-coalescing-boolean-props",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2141
|
|
19
|
+
],
|
|
20
|
+
"summary": "discount an absorbed comment from the chain-break decision (closes #2141)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.174",
|
|
26
|
+
"date": "2026-08-26T04:47:21.242Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-firestore-set-merge",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2140
|
|
33
|
+
],
|
|
34
|
+
"summary": "emit the appended option's separator before a trailing comment (closes #2140)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "no-usememo-for-pass-by-value",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
2138
|
|
41
|
+
],
|
|
42
|
+
"summary": "carry the trailing comment past the statement terminator (closes #2138)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "prefer-nullish-coalescing-boolean-props",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
2139
|
|
49
|
+
],
|
|
50
|
+
"summary": "carry the trailing comment past the statement terminator (closes #2139)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.173",
|
|
4
56
|
"date": "2026-08-25T23:22:38.601Z",
|