Skip to content

Commit a97ee63

Browse files
committed
fix(formatter): properly indent binary concatenation continuation lines in function arguments
closes #594 Signed-off-by: azjezz <[email protected]>
1 parent 9ea5bf8 commit a97ee63

File tree

6 files changed

+136
-50
lines changed

6 files changed

+136
-50
lines changed

crates/formatter/src/internal/format/binaryish.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,35 @@ pub(super) fn print_binaryish_expression<'arena>(
132132
)
133133
);
134134

135+
let is_breaking_concat_in_arg = operator.is_concatenation()
136+
&& matches!(grandparent, Some(Node::PositionalArgument(_) | Node::NamedArgument(_)))
137+
&& (matches!(left, Expression::Call(_)) || matches!(right, Expression::Call(_)));
138+
139+
if is_breaking_concat_in_arg {
140+
let has_space_around = match operator {
141+
BinaryishOperator::Binary(BinaryOperator::StringConcat(_)) => {
142+
f.settings.space_around_concatenation_binary_operator
143+
}
144+
_ => true,
145+
};
146+
let group_id = f.next_id();
147+
148+
return Document::Group(
149+
Group::new(vec![
150+
in f.arena;
151+
left.format(f),
152+
Document::IndentIfBreak(IndentIfBreak::new(group_id, vec![
153+
in f.arena;
154+
Document::Line(if has_space_around { Line::default() } else { Line::soft() }),
155+
format_token(f, operator.span(), operator.as_str()),
156+
Document::String(if has_space_around { " " } else { "" }),
157+
right.format(f),
158+
])),
159+
])
160+
.with_id(group_id),
161+
);
162+
}
163+
135164
let parts = print_binaryish_expression_parts(f, left, operator, right, is_inside_parenthesis, false);
136165

137166
if is_inside_parenthesis {

crates/formatter/src/internal/format/misc.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ pub(super) fn should_hug_expression<'arena>(
121121
}
122122

123123
if binary.operator.is_concatenation() {
124+
if matches!(binary.lhs, Expression::Call(_)) || matches!(binary.rhs, Expression::Call(_)) {
125+
return false;
126+
}
127+
124128
return (is_left_hand_side_simple && should_hug_expression(f, binary.rhs, arrow_function_recursion))
125129
|| (is_right_hand_side_simple && should_hug_expression(f, binary.lhs, arrow_function_recursion));
126130
}
@@ -209,6 +213,16 @@ pub fn is_breaking_expression<'arena>(
209213
)
210214
}
211215

216+
fn contains_breaking_concatenation(node: &Expression<'_>) -> bool {
217+
match node {
218+
Expression::Binary(binary) if binary.operator.is_concatenation() => {
219+
matches!(binary.lhs, Expression::Call(_)) || matches!(binary.rhs, Expression::Call(_))
220+
}
221+
Expression::Parenthesized(inner) => contains_breaking_concatenation(inner.expression),
222+
_ => false,
223+
}
224+
}
225+
212226
pub fn is_expandable_expression<'arena>(node: &'arena Expression<'arena>, include_calls: bool) -> bool {
213227
if let Expression::Parenthesized(inner) = node {
214228
return is_expandable_expression(inner.expression, include_calls);
@@ -570,7 +584,19 @@ pub(super) fn print_condition<'arena>(
570584
let was_in_condition = f.in_condition;
571585
f.in_condition = true;
572586

587+
let has_breaking_concat = match condition {
588+
Expression::Call(call) => {
589+
call.get_argument_list().arguments.iter().any(|arg| contains_breaking_concatenation(arg.value()))
590+
}
591+
Expression::Instantiation(inst) => inst
592+
.argument_list
593+
.as_ref()
594+
.is_some_and(|args| args.arguments.iter().any(|arg| contains_breaking_concatenation(arg.value()))),
595+
_ => false,
596+
};
597+
573598
let condition = if is_expandable_expression(condition, true)
599+
&& !has_breaking_concat
574600
&& !f.has_comment(condition.span(), CommentFlags::Leading | CommentFlags::Trailing)
575601
{
576602
Document::Group(Group::new(vec![
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
if (
6+
file_exists(
7+
dirname(__DIR__)
8+
. '/var/cache/prod/App_KernelProdContainer.preload.php',
9+
)
10+
) {
11+
include_file(
12+
dirname(__DIR__)
13+
. '/var/cache/prod/App_KernelProdContainer.preload.php',
14+
);
15+
16+
require
17+
dirname(__DIR__)
18+
. '/var/cache/prod/App_KernelProdContainer.preload.php';
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
if (file_exists(dirname(__DIR__)
6+
. '/var/cache/prod/App_KernelProdContainer.preload.php')) {
7+
include_file(
8+
dirname(__DIR__)
9+
. '/var/cache/prod/App_KernelProdContainer.preload.php'
10+
);
11+
12+
require
13+
dirname(__DIR__)
14+
. '/var/cache/prod/App_KernelProdContainer.preload.php';
15+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FormatSettings {
2+
print_width: 80,
3+
..Default::default()
4+
}

crates/formatter/tests/mod.rs

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,48 @@ test_case!(unary_prefix_prec);
166166
test_case!(rescue);
167167
test_case!(yield_kv_closure);
168168
test_case!(symfony_form_builder);
169+
test_case!(format_ignore_single_line_comment);
170+
test_case!(format_ignore_block_comment);
171+
test_case!(format_ignore_docblock);
172+
test_case!(format_ignore_hash_comment);
173+
test_case!(format_ignore_range_basic);
174+
test_case!(format_ignore_range_no_end);
175+
test_case!(format_ignore_range_multiple);
176+
test_case!(format_ignore_range_block_comment);
177+
test_case!(format_ignore_range_multi_statements);
178+
test_case!(format_ignore_next_basic);
179+
test_case!(format_ignore_next_multiple);
180+
test_case!(class_member_format_ignore_next);
181+
test_case!(class_member_format_ignore_region);
182+
test_case!(class_member_format_ignore_next_trait_use);
183+
test_case!(class_member_format_ignore_next_enum_case);
184+
test_case!(class_member_format_ignore_next_with_attributes);
185+
test_case!(class_member_format_ignore_region_mixed_types);
186+
test_case!(class_member_format_ignore_in_trait);
187+
test_case!(class_member_format_ignore_in_interface);
188+
test_case!(class_member_format_ignore_in_enum);
189+
test_case!(class_member_format_ignore_in_anonymous_class);
190+
test_case!(class_member_format_ignore_next_last_member);
191+
test_case!(class_member_format_ignore_next_multiple_consecutive);
192+
test_case!(class_member_format_ignore_region_entire_body);
193+
test_case!(class_member_format_ignore_region_single_member);
194+
test_case!(class_member_format_ignore_region_standalone_comment);
195+
test_case!(class_member_format_ignore_next_abstract_method);
196+
test_case!(class_member_format_ignore_next_static_members);
197+
test_case!(class_member_format_ignore_next_readonly_property);
198+
test_case!(class_member_format_ignore_next_promoted_property);
199+
test_case!(class_member_format_ignore_block_comment);
200+
test_case!(class_member_format_ignore_hash_comment);
201+
test_case!(class_member_format_ignore_docblock);
202+
test_case!(class_member_format_ignore_multiple_regions);
203+
test_case!(class_member_format_ignore_nested_class);
204+
test_case!(class_member_format_ignore_next_final_method);
205+
test_case!(class_member_format_ignore_next_visibility_variations);
206+
test_case!(class_member_format_ignore_region_with_comments);
207+
test_case!(class_member_format_ignore_next_hooked_property);
208+
test_case!(class_member_format_ignore_next_interface_constant);
209+
test_case!(class_member_format_ignore_next_backed_enum);
210+
test_case!(class_member_format_ignore_in_nested_anonymous_class);
169211

170212
// A special test case for regressions in the Psl codebase
171213
test_case!(psl_regressions);
@@ -241,63 +283,14 @@ test_case!(issue_503);
241283
test_case!(issue_545);
242284
test_case!(issue_536);
243285
test_case!(issue_556);
286+
test_case!(issue_594);
244287
test_case!(issue_602);
245288
test_case!(issue_634);
246289
test_case!(issue_640);
247290
test_case!(issue_683);
248291
test_case!(issue_727);
249292
test_case!(issue_738);
250293

251-
// Format ignore tests
252-
test_case!(format_ignore_single_line_comment);
253-
test_case!(format_ignore_block_comment);
254-
test_case!(format_ignore_docblock);
255-
test_case!(format_ignore_hash_comment);
256-
257-
// Range-based format ignore tests
258-
test_case!(format_ignore_range_basic);
259-
test_case!(format_ignore_range_no_end);
260-
test_case!(format_ignore_range_multiple);
261-
test_case!(format_ignore_range_block_comment);
262-
test_case!(format_ignore_range_multi_statements);
263-
264-
// Format ignore-next tests
265-
test_case!(format_ignore_next_basic);
266-
test_case!(format_ignore_next_multiple);
267-
268-
// Class member format ignore tests
269-
test_case!(class_member_format_ignore_next);
270-
test_case!(class_member_format_ignore_region);
271-
test_case!(class_member_format_ignore_next_trait_use);
272-
test_case!(class_member_format_ignore_next_enum_case);
273-
test_case!(class_member_format_ignore_next_with_attributes);
274-
test_case!(class_member_format_ignore_region_mixed_types);
275-
test_case!(class_member_format_ignore_in_trait);
276-
test_case!(class_member_format_ignore_in_interface);
277-
test_case!(class_member_format_ignore_in_enum);
278-
test_case!(class_member_format_ignore_in_anonymous_class);
279-
test_case!(class_member_format_ignore_next_last_member);
280-
test_case!(class_member_format_ignore_next_multiple_consecutive);
281-
test_case!(class_member_format_ignore_region_entire_body);
282-
test_case!(class_member_format_ignore_region_single_member);
283-
test_case!(class_member_format_ignore_region_standalone_comment);
284-
test_case!(class_member_format_ignore_next_abstract_method);
285-
test_case!(class_member_format_ignore_next_static_members);
286-
test_case!(class_member_format_ignore_next_readonly_property);
287-
test_case!(class_member_format_ignore_next_promoted_property);
288-
test_case!(class_member_format_ignore_block_comment);
289-
test_case!(class_member_format_ignore_hash_comment);
290-
test_case!(class_member_format_ignore_docblock);
291-
test_case!(class_member_format_ignore_multiple_regions);
292-
test_case!(class_member_format_ignore_nested_class);
293-
test_case!(class_member_format_ignore_next_final_method);
294-
test_case!(class_member_format_ignore_next_visibility_variations);
295-
test_case!(class_member_format_ignore_region_with_comments);
296-
test_case!(class_member_format_ignore_next_hooked_property);
297-
test_case!(class_member_format_ignore_next_interface_constant);
298-
test_case!(class_member_format_ignore_next_backed_enum);
299-
test_case!(class_member_format_ignore_in_nested_anonymous_class);
300-
301294
#[test]
302295
fn test_all_test_cases_are_ran() {
303296
let test_case_file = include_str!("mod.rs");

0 commit comments

Comments
 (0)