fix(formatter): keep a comment before } inside a JSX spread child (#28103)

`jsx/spread/child.js` is in the "Not idempotent" list of both Prettier
conformance reports. Its own-line comment before `}` survives the first
pass, but on the second pass it moves out of the spread child:

```jsx
// pass 1 (same as Prettier)
<div>
  {
    ...a
    //comment
  }
</div>;

// pass 2
<div>
  {
    ...a
  }
//comment
</div>;
```

`JSXExpressionContainer` prints the comments before `}` inside its
braces
(`FormatTrailingComments::Comments(comments_before(self.span.end))`), so
`{a //comment}` is stable; `JSXSpreadChild` printed only the leading
comments and the expression, so once the child sits on its own lines the
comment is left for the parent to print after `}`. This adds the same
trailing-comment print to `JSXSpreadChild`. (`JSXSpreadAttribute`
already keeps it.)

Tests:
- New fixture `js/jsx/spread-child-trailing-comment.jsx`; it fails on
`main`, and its output is identical to Prettier 3.9.10 (the pinned
version) at both option sets and stable on a second pass.
- `jsx/spread/child.js` drops out of the "Not idempotent" lists in
`conformance__prettier-js.snap` and `conformance__prettier-ts.snap`; no
other conformance change.
- `cargo test -p oxc_formatter --all-features`, `cargo fmt --check`,
`just lint -p oxc_formatter` (clippy with warnings denied) and `typos`
pass.

AI disclosure: this PR (code, tests and description) was written with
Claude Code (Claude Opus 5.5); the verification above is what was run.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: leaysgur <6259812+leaysgur@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-26 07:10:45 +00:00
1 parent 190bb1e67f
commit 016c4a2a0d
5 files changed
+130 -36

No files matched your search

+31 -34
View File
@@ -336,23 +336,7 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, JSXAttribute<'a>> {
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, JSXSpreadAttribute<'a>> {
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
let comments = f.context().comments();
let has_comment = comments.has_comment_before(self.argument.span().start)
|| comments.has_comment_in_range(self.argument.span().end, self.span.end);
let format_inner = format_with(|f| {
write!(f, [format_leading_comments(self.argument.span()), "..."]);
self.argument().fmt(f);
});
write!(f, ["{"]);
if has_comment {
write!(f, [soft_block_indent(&format_inner)]);
} else {
write!(f, [format_inner]);
}
write!(f, "}");
write_jsx_spread(self.span, self.argument(), f);
}
}
@@ -364,26 +348,39 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, JSXIdentifier<'a>> {
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, JSXSpreadChild<'a>> {
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
let comments = f.context().comments();
let has_comment = comments.has_comment_before(self.expression.span().start)
|| comments.has_comment_in_range(self.expression.span().end, self.span.end);
let format_inner = format_with(|f| {
write!(f, [format_leading_comments(self.expression.span()), "..."]);
self.expression().fmt(f);
});
write!(f, "{");
if has_comment {
write!(f, [soft_block_indent(&format_inner)]);
} else {
write!(f, [format_inner]);
}
write!(f, "}");
write_jsx_spread(self.span, self.expression(), f);
}
}
/// Writes `{...argument}` for a spread attribute or child.
fn write_jsx_spread<'a>(
span: Span,
argument: AstNode<'_, 'a, Expression<'a>>,
f: &mut JsFormatter<'_, 'a>,
) {
let argument_span = argument.span();
let comments = f.context().comments();
let has_comment = comments.has_comment_before(argument_span.start)
|| comments.has_comment_in_range(argument_span.end, span.end);
let format_inner = format_with(|f| {
write!(f, [format_leading_comments(argument_span), "..."]);
argument.fmt(f);
// Keep the comments before `}` inside the braces, like `JSXExpressionContainer`
let comments = f.context().comments().comments_before(span.end);
write!(f, [FormatTrailingComments::Comments(comments)]);
});
write!(f, "{");
if has_comment {
write!(f, [soft_block_indent(&format_inner)]);
} else {
write!(f, [format_inner]);
}
write!(f, "}");
}
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, JSXText<'a>> {
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
write!(f, text(self.value().as_str()));
@@ -0,0 +1,21 @@
// A comment before `}` stays inside the braces of a spread child or attribute.
<div>
{
...a
//comment
}
</div>;
<div>
{
...a
/* comment */
}
</div>;
<div
{
...a
//comment
}
/>;
@@ -0,0 +1,78 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// A comment before `}` stays inside the braces of a spread child or attribute.
<div>
{
...a
//comment
}
</div>;
<div>
{
...a
/* comment */
}
</div>;
<div
{
...a
//comment
}
/>;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// A comment before `}` stays inside the braces of a spread child or attribute.
<div>
{
...a
//comment
}
</div>;
<div>
{
...a
/* comment */
}
</div>;
<div
{
...a
//comment
}
/>;
-------------------
{ printWidth: 100 }
-------------------
// A comment before `}` stays inside the braces of a spread child or attribute.
<div>
{
...a
//comment
}
</div>;
<div>
{
...a
/* comment */
}
</div>;
<div
{
...a
//comment
}
/>;
===================== End =====================
@@ -93,4 +93,3 @@ js compatibility: 1033/1123 (91.99%), 23 files skipped
- js/for/continue-and-break-comment-without-blocks.js
- js/function/issue-12967.js
- jsx/comments/in-attributes.js
- jsx/spread/child.js
@@ -73,6 +73,5 @@ ts compatibility: 754/819 (92.06%), 16 files skipped
# Not idempotent
- jsx/comments/in-attributes.js
- jsx/spread/child.js
- typescript/as/comments/17407.ts
- typescript/method-chain/object/issue-17239.ts