mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
5 files changed
+130
-36
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user