fix(formatter): glue comments to an opener in empty for-head slots and array holes (#27737)

Follow up on #27730,
Use the same utils with `for (/*c*/;;)` case.

Also fixed some weird comment printing.
This commit is contained in:
leaysgur committed 2026-10-16 00:58:42 +00:00
1 parent 6a95ec188b
commit d2bea21099
8 files changed
+134 -47

No files matched your search

+9 -34
View File
@@ -3,14 +3,11 @@ use oxc_span::{GetSpan, SPAN, Span};
use crate::{
ast_nodes::AstNode,
format_args,
formatter::{
prelude::*,
trivia::{FormatCommentBeforeContent, format_trailing_comments},
},
formatter::{prelude::*, trivia::format_trailing_comments},
options::FormatTrailingCommas,
utils::{
format_node_without_trailing_comments::FormatNodeWithoutTrailingComments,
statement_body::write_trailing_comments_before,
statement_body::write_trailing_comments,
},
write,
};
@@ -109,7 +106,7 @@ pub fn write_array_node<'a, T>(
join.entry(
entry_span,
&format_once(|f| {
write_hole_slot_comments(span.start, f);
write_unclaimed_comments(span.start, f);
write!(f, ",");
if let Some(following) = following {
write!(f, format_trailing_comments(range, span, following));
@@ -125,35 +122,13 @@ pub fn write_array_node<'a, T>(
}
}
/// Comments in a hole's slot, before its `,` at `comma`.
/// A hole has no content, so the `,` follows a block comment directly (`[a, /* c */, b]`),
/// as a separator follows any content (`[a /* c */, b]`); an own-line comment keeps its line.
///
/// Not [`write_trailing_comments_before`] like an empty for-head slot:
/// it would space a comment away from the `[` (`[ /* c */, b]`),
/// and let a line comment right after the `[` ride past the hole's `,` (`[// c` + break + `, b]` -> `, // c`).
fn write_hole_slot_comments(comma: u32, f: &mut JsFormatter<'_, '_>) {
let comments = f.context().comments().comments_before(comma);
for (index, comment) in comments.iter().enumerate() {
// A line comment ends with a hard break
write!(f, FormatCommentBeforeContent::new(comment));
if comment.is_block() {
if comment.followed_by_newline() {
write!(f, hard_line_break());
} else if index + 1 < comments.len() {
write!(f, space());
}
}
}
}
/// Comments that no element consumed (e.g. after a trailing hole: `[,, /* comment */]`)
/// would otherwise escape the brackets; print them before the `]` at `end`,
/// trailing the last `,` like after any separator (`[, , /* comment */]`).
/// Prints the comments no element consumed before `end`, in place:
/// a hole's slot before its `,` (`[a, /* comment */, b]`),
/// or the comments before the `]` that would otherwise escape the brackets (`[, , /* comment */]`).
pub fn write_unclaimed_comments(end: u32, f: &mut JsFormatter<'_, '_>) {
let Some(first) = f.context().comments().comments_before(end).first() else { return };
let comments = f.context().comments().comments_before(end);
// Discarded flush obligation:
// a riding line comment's `expand_parent` breaks the array group,
// so a real break already follows before the `]`
let _line_comment_riding = write_trailing_comments_before(first.span.start, b']', f);
// so a real break already follows the `,` or comes before the `]`
let _line_comment_riding = write_trailing_comments(comments, f);
}
@@ -119,9 +119,30 @@ pub fn write_trailing_comments_before(
f: &mut JsFormatter<'_, '_>,
) -> bool {
let comments = f.context().comments().comments_before_character(start, character);
write_trailing_comments(comments, f)
}
/// [`write_trailing_comments_before`] for an already bounded run of pending `comments`
/// (e.g. an array hole's before its `,`).
///
/// Right after an opener (`(`, `[`) nothing precedes to trail, so every comment prints in place:
/// same-line block comments glue to it (`for (/* c */;;)`, `[/* c */, a]`),
/// and a same-line run ending in a line comment moves to its own line as a whole (the opener exception).
#[must_use = "a riding line comment needs a line break right after the caller's token"]
pub fn write_trailing_comments<'a>(comments: &'a [Comment], f: &mut JsFormatter<'_, 'a>) -> bool {
let Some(first) = comments.first() else { return false };
let same_line_count =
comments.iter().take_while(|comment| !comment.preceded_by_newline()).count();
let (same_line, own_line) = comments.split_at(same_line_count);
let after_opener =
matches!(f.source_text().previous_non_whitespace_byte(first.span.start), Some(b'(' | b'['));
let (same_line, in_place) = if after_opener {
if comments[..same_line_count].last().is_some_and(Comment::is_line) {
write!(f, hard_line_break());
}
(&[][..], comments)
} else {
comments.split_at(same_line_count)
};
FormatTrailingComments::Comments(same_line).fmt(f);
// Own-line comments print in place, never via `line_suffix`:
@@ -130,16 +151,21 @@ pub fn write_trailing_comments_before(
// as the own-line invariant requires;
// the caller's token follows a block comment directly (`/* c */;`),
// the stable form its own broken output re-parses to.
for comment in own_line {
if f.lines_before(comment.span) > 1 {
for (index, comment) in in_place.iter().enumerate() {
if !comment.preceded_by_newline() {
// Glued to the opener, or continuing the previous comment's line (`/* a */ // b`)
if index > 0 {
write!(f, space());
}
} else if f.lines_before(comment.span) > 1 {
write!(f, empty_line());
} else {
write!(f, hard_line_break());
}
write!(f, FormatCommentBeforeContent::new(comment));
}
// The own-line loop's breaks have already flushed a pending same-line line comment
own_line.is_empty() && same_line.last().is_some_and(Comment::is_line)
// The in-place loop's breaks have already flushed a pending same-line line comment
in_place.is_empty() && same_line.last().is_some_and(Comment::is_line)
}
/// Comments between a block's `}` and a following keyword (`else`/`catch`/`finally`)
+1 -4
View File
@@ -128,10 +128,7 @@ pub fn is_cast_target(span: Span, f: &JsFormatter<'_, '_>) -> bool {
// A target is preceded by its `(` or by the `/` closing a comment:
// one byte peek rejects most nodes before the `)` peek (true for every last argument and parenthesized test)
// and the comment lookup.
if !matches!(
source.bytes_to(span.start).find(|byte| !byte.is_ascii_whitespace()),
Some(b'(' | b'/')
) {
if !matches!(source.previous_non_whitespace_byte(span.start), Some(b'(' | b'/')) {
return false;
}
let comments = f.context().comments();
@@ -43,3 +43,8 @@ const m = [/** @type {X} */ (a) /* comment */, , b];
const n = [/* lead */, 2];
const o = [1, /* c1 */, /* c2 */, 4];
const p = [1 /* t */, /* slot */, 3];
// Right after the `[`, a same-line run ending in a line comment moves to its own line as a whole
const q = [// comment
, b];
const r = [/* a */ // b
, b];
@@ -47,6 +47,11 @@ const m = [/** @type {X} */ (a) /* comment */, , b];
const n = [/* lead */, 2];
const o = [1, /* c1 */, /* c2 */, 4];
const p = [1 /* t */, /* slot */, 3];
// Right after the `[`, a same-line run ending in a line comment moves to its own line as a whole
const q = [// comment
, b];
const r = [/* a */ // b
, b];
==================== Output ====================
------------------
@@ -100,6 +105,17 @@ const m = [/** @type {X} */ (a) /* comment */, , b];
const n = [/* lead */, 2];
const o = [1, /* c1 */, /* c2 */, 4];
const p = [1 /* t */, /* slot */, 3];
// Right after the `[`, a same-line run ending in a line comment moves to its own line as a whole
const q = [
// comment
,
b,
];
const r = [
/* a */ // b
,
b,
];
-------------------
{ printWidth: 100 }
@@ -152,5 +168,16 @@ const m = [/** @type {X} */ (a) /* comment */, , b];
const n = [/* lead */, 2];
const o = [1, /* c1 */, /* c2 */, 4];
const p = [1 /* t */, /* slot */, 3];
// Right after the `[`, a same-line run ending in a line comment moves to its own line as a whole
const q = [
// comment
,
b,
];
const r = [
/* a */ // b
,
b,
];
===================== End =====================
@@ -25,3 +25,13 @@ for (a // c1
; b; c) {}
for (a; // t
b; c) {}
// Right after the `(`, a same-line run ending in a line comment moves to its own line as a whole (the opener exception),
// and own-line comments sharing a line stay together
for (// c
;;) {}
for (/* a */ // b
;;) {}
for (;
/* a */ // b
;) {}
@@ -30,6 +30,16 @@ for (a // c1
for (a; // t
b; c) {}
// Right after the `(`, a same-line run ending in a line comment moves to its own line as a whole (the opener exception),
// and own-line comments sharing a line stay together
for (// c
;;) {}
for (/* a */ // b
;;) {}
for (;
/* a */ // b
;) {}
==================== Output ====================
------------------
{ printWidth: 80 }
@@ -49,10 +59,10 @@ for (a; ; /* c */) {}
for (a /* c */; ;);
for (a; b /* c */;) {}
for (; /*t*/ b;);
for ( /*s0*/;;);
for (/*s0*/;;);
for (; /*s1*/;) {}
for (;; /*s2*/);
for ( /*s0*/; /*s1*/; /*s2*/);
for (/*s0*/; /*s1*/; /*s2*/);
for (;;) /* after */ ;
// A line comment around a `;` trails its slot's line and stays there
@@ -68,6 +78,22 @@ for (
c
) {}
// Right after the `(`, a same-line run ending in a line comment moves to its own line as a whole (the opener exception),
// and own-line comments sharing a line stay together
for (
// c
;;
) {}
for (
/* a */ // b
;;
) {}
for (
;
/* a */ // b
;
) {}
-------------------
{ printWidth: 100 }
-------------------
@@ -86,10 +112,10 @@ for (a; ; /* c */) {}
for (a /* c */; ;);
for (a; b /* c */;) {}
for (; /*t*/ b;);
for ( /*s0*/;;);
for (/*s0*/;;);
for (; /*s1*/;) {}
for (;; /*s2*/);
for ( /*s0*/; /*s1*/; /*s2*/);
for (/*s0*/; /*s1*/; /*s2*/);
for (;;) /* after */ ;
// A line comment around a `;` trails its slot's line and stays there
@@ -105,4 +131,20 @@ for (
c
) {}
// Right after the `(`, a same-line run ending in a line comment moves to its own line as a whole (the opener exception),
// and own-line comments sharing a line stay together
for (
// c
;;
) {}
for (
/* a */ // b
;;
) {}
for (
;
/* a */ // b
;
) {}
===================== End =====================
@@ -71,6 +71,11 @@ impl<'a> SourceText<'a> {
self.next_byte_skipping(position, |byte| byte.is_ascii_whitespace())
}
/// Last non-whitespace byte before position
pub fn previous_non_whitespace_byte(&self, position: u32) -> Option<u8> {
self.bytes_to(position).find(|byte| !byte.is_ascii_whitespace())
}
/// First byte at or after position that `skip` rejects
#[inline]
pub fn next_byte_skipping(&self, position: u32, skip: impl Fn(u8) -> bool) -> Option<u8> {