From fb3aefa8d647dd6c360703591758a13fd589688f Mon Sep 17 00:00:00 2001
From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com>
Date: Fri, 30 Oct 2026 16:53:03 +0900
Subject: [PATCH] fix(formatter): keep source parens around html-in-js
placeholders past comments (#28381)
It's a rare case, but just to be safe.
---
.../placeholder-parens-with-comment.js | 3 ++
.../conformance/snapshots/conformance.snap.md | 4 +--
...tml-in-js__placeholder-case-in-child.js.md | 4 +--
.../src/parentheses/expression.rs | 30 ++++++++++++-------
4 files changed, 27 insertions(+), 14 deletions(-)
create mode 100644 apps/oxfmt/conformance/fixtures/edge-cases/html-in-js/placeholder-parens-with-comment.js
diff --git a/apps/oxfmt/conformance/fixtures/edge-cases/html-in-js/placeholder-parens-with-comment.js b/apps/oxfmt/conformance/fixtures/edge-cases/html-in-js/placeholder-parens-with-comment.js
new file mode 100644
index 0000000000..b3e367b519
--- /dev/null
+++ b/apps/oxfmt/conformance/fixtures/edge-cases/html-in-js/placeholder-parens-with-comment.js
@@ -0,0 +1,3 @@
+html``;
+html``;
+html``;
diff --git a/apps/oxfmt/conformance/snapshots/conformance.snap.md b/apps/oxfmt/conformance/snapshots/conformance.snap.md
index 8f3fcb8c2a..0b652e8ed2 100644
--- a/apps/oxfmt/conformance/snapshots/conformance.snap.md
+++ b/apps/oxfmt/conformance/snapshots/conformance.snap.md
@@ -74,7 +74,7 @@
## html-in-js
-### Option 1: 172/200 (86.00%)
+### Option 1: 173/201 (86.07%)
```json
{"printWidth":80}
@@ -137,7 +137,7 @@
- [externals/webawesome/textarea/textarea.ts](diffs/html-in-js/externals__webawesome__textarea__textarea.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
-### Option 2: 186/200 (93.00%)
+### Option 2: 187/201 (93.03%)
```json
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
diff --git a/apps/oxfmt/conformance/snapshots/diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md b/apps/oxfmt/conformance/snapshots/diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md
index 2fad0a234a..51f8e09831 100644
--- a/apps/oxfmt/conformance/snapshots/diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md
+++ b/apps/oxfmt/conformance/snapshots/diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md
@@ -74,7 +74,7 @@ const d = html`
@@ -110,7 +110,7 @@ const d = html`
const a = html`
`;
diff --git a/crates/oxc_formatter/src/parentheses/expression.rs b/crates/oxc_formatter/src/parentheses/expression.rs
index 2038d47ae1..f40663eee4 100644
--- a/crates/oxc_formatter/src/parentheses/expression.rs
+++ b/crates/oxc_formatter/src/parentheses/expression.rs
@@ -144,23 +144,33 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, IdentifierReference<'_>> {
/// Whether the source wraps `node` in parentheses of its own (the AST keeps none),
/// besides those of the syntax around it (a sole call argument, an `if` test, ...).
+/// Comments between them are skipped (`f((a) /* c */)`).
+#[expect(clippy::cast_possible_truncation)] // Offsets fit in `u32`, source length does
fn is_parenthesized_in_source(
node: &AstNode<'_, '_, IdentifierReference<'_>>,
f: &JsFormatter<'_, '_>,
) -> bool {
let bytes = f.source_text().as_str().as_bytes();
- let (mut start, mut end) = (node.span.start as usize, node.span.end as usize);
- let mut pairs = 0;
- loop {
- let before = bytes[..start].iter().rposition(|b| !b.is_ascii_whitespace());
- let after = bytes[end..].iter().position(|b| !b.is_ascii_whitespace()).map(|i| end + i);
- match (before, after) {
- (Some(before), Some(after)) if bytes[before] == b'(' && bytes[after] == b')' => {
- pairs += 1;
- (start, end) = (before, after + 1);
+ let comments = f.context().comments();
+ // The last byte before `pos` that is neither whitespace nor in a comment
+ let before = |mut pos: u32| -> Option {
+ loop {
+ let i = bytes[..pos as usize].iter().rposition(|b| !b.is_ascii_whitespace())? as u32;
+ match comments.all_comments_before(pos).last() {
+ Some(comment) if comment.span.end > i => pos = comment.span.start,
+ _ => return Some(i),
}
- _ => break,
}
+ };
+ let (mut start, mut end) = (node.span.start, node.span.end);
+ let mut pairs = 0;
+ while let Some(open) = before(start)
+ && bytes[open as usize] == b'('
+ && comments.is_followed_by_closing_paren(end)
+ {
+ pairs += 1;
+ start = open;
+ end = comments.position_after_character(end, b')');
}
let span = node.span;
let syntax_parens = match node.parent() {