Skip to content

Commit 7ba5543

Browse files
jsmechamclaude
andcommitted
fix(formatter): break call args instead of = for hugged template assignments
When the right-hand side of an assignment is a graphql/template hug call (`graphql(`...`)`, `fn(/* lang */ `...`)`, etc.) where the template was already multi-line in the source, prefer breaking the call's arguments out over breaking at `=` when the hugged form would overflow the print width. Single-line templates that get rewritten to multi-line by the embedded formatter still fall back to the unconditional hug, which lets will_break propagate to break the assignment at `=` — matching Prettier's `breakParent` behavior for embed labels. Member chains keep an unconditional hug so the surrounding group decides. Fixes #21908 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4380812 commit 7ba5543

3 files changed

Lines changed: 157 additions & 23 deletions

File tree

crates/oxc_formatter/src/print/call_like_expression/arguments.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ use oxc_span::GetSpan;
55
use crate::{
66
Buffer, Format, FormatTrailingCommas, TrailingSeparator,
77
ast_nodes::{AstNode, AstNodes},
8-
format_args,
8+
best_fitting, format_args,
99
formatter::{
1010
Comments, FormatElement, Formatter, SourceText, VecBuffer,
1111
buffer::RemoveSoftLinesBuffer,
1212
format_element,
1313
prelude::{
14-
FormatElements, Tag, empty_line, expand_parent, format_once, format_with, group,
15-
soft_block_indent, soft_line_break_or_space, space,
14+
BestFitting, FormatElements, MemoizeFormat, Tag, empty_line, expand_parent,
15+
format_once, format_with, group, soft_block_indent, soft_line_break_or_space, space,
1616
},
1717
trivia::format_dangling_comments,
1818
},
@@ -62,7 +62,7 @@ impl<'a> Format<'a> for AstNode<'a, ArenaVec<'a, Argument<'a>>> {
6262
None
6363
};
6464

65-
if is_simple_module_import
65+
let always_hug = is_simple_module_import
6666
|| call_expression.is_some_and(|call| {
6767
is_commonjs_or_amd_call(self, call, f)
6868
|| ((self.len() != 2
@@ -76,25 +76,48 @@ impl<'a> Format<'a> for AstNode<'a, ArenaVec<'a, Argument<'a>>> {
7676
))
7777
&& is_test_call_expression(call))
7878
})
79-
|| is_multiline_template_only_args(self, f.source_text())
80-
|| is_graphql_call_with_single_template_arg(self, call_expression)
81-
|| is_huggable_html_embed_single_arg(self, f)
82-
|| is_react_hook_with_deps_array(self, f.comments())
83-
{
84-
return write!(
85-
f,
86-
[
87-
l_paren_token,
88-
format_with(|f| {
89-
f.join_with(space()).entries_with_trailing_separator(
90-
self.iter(),
91-
",",
92-
TrailingSeparator::Omit,
93-
);
94-
}),
95-
r_paren_token
96-
]
97-
);
79+
|| is_react_hook_with_deps_array(self, f.comments());
80+
81+
let template_can_hug = !always_hug
82+
&& (is_multiline_template_only_args(self, f.source_text())
83+
|| is_graphql_call_with_single_template_arg(self, call_expression)
84+
|| is_huggable_html_embed_single_arg(self, f));
85+
86+
if always_hug || template_can_hug {
87+
// For an assignment-like RHS where the template was *already* multi-line in the
88+
// source, prefer breaking call args over breaking at `=` when the hugged form
89+
// would overflow. Single-line templates that get rewritten to multi-line by the
90+
// embedded formatter still break at `=` (Prettier emits `breakParent` there).
91+
// Other contexts (e.g. member chains) keep an unconditional hug.
92+
let break_when_overflow = template_can_hug
93+
&& is_multiline_template_only_args(self, f.source_text())
94+
&& call_expression.is_some_and(|call| {
95+
matches!(
96+
call.parent(),
97+
AstNodes::VariableDeclarator(_) | AstNodes::AssignmentExpression(_)
98+
)
99+
});
100+
101+
let hugged = format_with(|f| {
102+
f.join_with(space()).entries_with_trailing_separator(
103+
self.iter(),
104+
",",
105+
TrailingSeparator::Omit,
106+
);
107+
})
108+
.memoized();
109+
110+
if break_when_overflow {
111+
return write!(
112+
f,
113+
[best_fitting![
114+
format_args!(l_paren_token, hugged, r_paren_token),
115+
format_args!(l_paren_token, soft_block_indent(&hugged), r_paren_token),
116+
]]
117+
);
118+
}
119+
120+
return write!(f, [l_paren_token, hugged, r_paren_token]);
98121
}
99122

100123
// Check if there's an empty line (2+ newlines) between any consecutive arguments.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Short LHS — hugged form fits, no break at `=` and no break around args.
2+
const a = graphql(`
3+
q
4+
`);
5+
6+
// Short LHS with leading comment on the template — still hugs.
7+
const b = graphql(/* GraphQL */ `
8+
q
9+
`);
10+
11+
// Long LHS — hugged form would overflow; break args instead of `=`.
12+
const longlonglonglonglonglonglonglonglonglonglonglonglong = graphql(/* GraphQL */ `
13+
q
14+
`);
15+
16+
// Member chain — chain breaks at the dot, args stay hugged.
17+
foo(bar).baz(`a long first line of template content that pushes the call past 80 cols
18+
`);
19+
20+
// Generic single-arg call with a multiline template (non-graphql).
21+
const c = fn(`
22+
q
23+
`);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
source: crates/oxc_formatter/tests/fixtures/mod.rs
3+
assertion_line: 232
4+
---
5+
==================== Input ====================
6+
// Short LHS — hugged form fits, no break at `=` and no break around args.
7+
const a = graphql(`
8+
q
9+
`);
10+
11+
// Short LHS with leading comment on the template — still hugs.
12+
const b = graphql(/* GraphQL */ `
13+
q
14+
`);
15+
16+
// Long LHS — hugged form would overflow; break args instead of `=`.
17+
const longlonglonglonglonglonglonglonglonglonglonglonglong = graphql(/* GraphQL */ `
18+
q
19+
`);
20+
21+
// Member chain — chain breaks at the dot, args stay hugged.
22+
foo(bar).baz(`a long first line of template content that pushes the call past 80 cols
23+
`);
24+
25+
// Generic single-arg call with a multiline template (non-graphql).
26+
const c = fn(`
27+
q
28+
`);
29+
30+
==================== Output ====================
31+
------------------
32+
{ printWidth: 80 }
33+
------------------
34+
// Short LHS — hugged form fits, no break at `=` and no break around args.
35+
const a = graphql(`
36+
q
37+
`);
38+
39+
// Short LHS with leading comment on the template — still hugs.
40+
const b = graphql(/* GraphQL */ `
41+
q
42+
`);
43+
44+
// Long LHS — hugged form would overflow; break args instead of `=`.
45+
const longlonglonglonglonglonglonglonglonglonglonglonglong = graphql(
46+
/* GraphQL */ `
47+
q
48+
`
49+
);
50+
51+
// Member chain — chain breaks at the dot, args stay hugged.
52+
foo(bar)
53+
.baz(`a long first line of template content that pushes the call past 80 cols
54+
`);
55+
56+
// Generic single-arg call with a multiline template (non-graphql).
57+
const c = fn(`
58+
q
59+
`);
60+
61+
-------------------
62+
{ printWidth: 100 }
63+
-------------------
64+
// Short LHS — hugged form fits, no break at `=` and no break around args.
65+
const a = graphql(`
66+
q
67+
`);
68+
69+
// Short LHS with leading comment on the template — still hugs.
70+
const b = graphql(/* GraphQL */ `
71+
q
72+
`);
73+
74+
// Long LHS — hugged form would overflow; break args instead of `=`.
75+
const longlonglonglonglonglonglonglonglonglonglonglonglong = graphql(/* GraphQL */ `
76+
q
77+
`);
78+
79+
// Member chain — chain breaks at the dot, args stay hugged.
80+
foo(bar).baz(`a long first line of template content that pushes the call past 80 cols
81+
`);
82+
83+
// Generic single-arg call with a multiline template (non-graphql).
84+
const c = fn(`
85+
q
86+
`);
87+
88+
===================== End =====================

0 commit comments

Comments
 (0)