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() {