fix(formatter): keep source parens around html-in-js placeholders past comments (#28381)

It's a rare case, but just to be safe.
This commit is contained in:
Yuji Sugiura authored and GitHub committed 2026-10-30 16:53:03 +09:00
1 parent e738daede1
commit fb3aefa8d6
4 files changed
+27 -14

No files matched your search

@@ -0,0 +1,3 @@
html`<script>f((${a}) /* c */)</script>`;
html`<script>f((${a}))</script>`;
html`<script>f(/* c */ (${a}))</script>`;
@@ -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"}
@@ -74,7 +74,7 @@ const d = html`<style>
const a = html`
<style>
a {
- prettier_html_placeholder_0_2989_in_js: ${c};
- prettier_html_placeholder_0_2992_in_js: ${c};
+ ${b}: ${c};
}
</style>
@@ -110,7 +110,7 @@ const d = html`
const a = html`
<style>
a {
prettier_html_placeholder_0_2989_in_js: ${c};
prettier_html_placeholder_0_2992_in_js: ${c};
}
</style>
`;
@@ -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<u32> {
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() {