mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-06 17:34:25 +00:00
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:
1 parent
6a95ec188b
commit
d2bea21099
8 files changed
+134
-47
No files matched your search
@@ -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`)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in new issue
Block a user