Skip to content

Commit c2a8c13

Browse files
committed
fix(formatter): ensure idempotent formatting for comments inside nested parenthesized binary expressions
closes #812 Signed-off-by: azjezz <[email protected]>
1 parent 043978f commit c2a8c13

File tree

5 files changed

+82
-7
lines changed

5 files changed

+82
-7
lines changed

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

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use mago_syntax::ast::Expression;
1010
use mago_syntax::ast::LegacyArray;
1111
use mago_syntax::ast::List;
1212
use mago_syntax::ast::Node;
13-
use mago_syntax::ast::node;
13+
use mago_syntax::ast::NodeKind;
1414
use mago_syntax::token::GetPrecedence;
1515
use mago_syntax::token::Precedence;
16-
use node::NodeKind;
1716

1817
use crate::document::Document;
1918
use crate::document::Group;
@@ -27,6 +26,38 @@ use crate::internal::utils::is_at_call_like_expression;
2726
use crate::internal::utils::is_at_callee;
2827
use crate::internal::utils::unwrap_parenthesized;
2928

29+
/// Recursively finds the leftmost expression in a binary tree and checks if it has
30+
/// a leading own-line comment. This is needed for idempotent formatting because
31+
/// comments inside nested parentheses will "float up" to become leading comments
32+
/// after the parens are removed during formatting.
33+
fn has_leading_comment_in_leftmost(f: &FormatterState, expr: &Expression) -> bool {
34+
let expr = unwrap_parenthesized(expr);
35+
36+
if f.has_leading_own_line_comment(expr.span()) {
37+
return true;
38+
}
39+
40+
match expr {
41+
Expression::Binary(binary) => has_leading_comment_in_leftmost(f, binary.lhs),
42+
Expression::Conditional(Conditional { then: None, condition, .. }) => {
43+
has_leading_comment_in_leftmost(f, condition)
44+
}
45+
_ => false,
46+
}
47+
}
48+
49+
/// Gets the leftmost expression in a binary tree, recursively unwrapping
50+
/// parentheses and following the LHS of binary/elvis expressions.
51+
fn get_leftmost_expression<'a>(expr: &'a Expression<'a>) -> &'a Expression<'a> {
52+
let expr = unwrap_parenthesized(expr);
53+
54+
match expr {
55+
Expression::Binary(binary) => get_leftmost_expression(binary.lhs),
56+
Expression::Conditional(Conditional { then: None, condition, .. }) => get_leftmost_expression(condition),
57+
_ => expr,
58+
}
59+
}
60+
3061
/// An internal-only enum to represent operators that should be formatted
3162
/// like binary operators. This allows us to reuse the same complex formatting
3263
/// logic for both true `BinaryOperator`s and other constructs like the
@@ -111,6 +142,7 @@ pub(super) fn print_binaryish_expression<'arena>(
111142
operator: BinaryishOperator<'arena>,
112143
right: &'arena Expression<'arena>,
113144
) -> Document<'arena> {
145+
let original_right = right;
114146
let left = unwrap_parenthesized(left);
115147
let right = unwrap_parenthesized(right);
116148

@@ -161,7 +193,7 @@ pub(super) fn print_binaryish_expression<'arena>(
161193
);
162194
}
163195

164-
let parts = print_binaryish_expression_parts(f, left, operator, right, is_inside_parenthesis, false);
196+
let parts = print_binaryish_expression_parts(f, left, operator, original_right, is_inside_parenthesis, false);
165197

166198
if is_inside_parenthesis {
167199
let lhs_is_binary = left.is_binary();
@@ -233,11 +265,14 @@ fn print_binaryish_expression_parts<'arena>(
233265
is_nested: bool,
234266
) -> Vec<'arena, Document<'arena>> {
235267
let left = unwrap_parenthesized(left);
268+
let original_right = right;
236269
let right = unwrap_parenthesized(right);
270+
let is_original_right_parenthesized = !std::ptr::eq(original_right, right);
237271
let should_break = f
238272
.has_comment(operator.span(), CommentFlags::Trailing | CommentFlags::Leading | CommentFlags::Line)
239273
|| f.has_comment(left.span(), CommentFlags::Trailing | CommentFlags::Line)
240-
|| f.has_leading_own_line_comment(right.span());
274+
|| f.has_leading_own_line_comment(right.span())
275+
|| (is_original_right_parenthesized && has_leading_comment_in_leftmost(f, original_right));
241276

242277
let mut should_inline_this_level = !should_break && should_inline_binary_rhs_expression(f, right, &operator);
243278
should_inline_this_level = should_inline_this_level || f.is_in_inlined_binary_chain;
@@ -288,9 +323,19 @@ fn print_binaryish_expression_parts<'arena>(
288323
_ => true,
289324
};
290325

291-
let line_before_operator = f.settings.line_before_binary_operator && !f.has_leading_own_line_comment(right.span());
326+
let has_leading_comment_on_right = f.has_leading_own_line_comment(right.span())
327+
|| (is_original_right_parenthesized && has_leading_comment_in_leftmost(f, original_right));
328+
let line_before_operator = f.settings.line_before_binary_operator && !has_leading_comment_on_right;
292329
let operator_has_leading_comments = f.has_comment(operator.span(), CommentFlags::Leading);
293330

331+
let leftmost_leading_comments =
332+
if is_original_right_parenthesized && has_leading_comment_in_leftmost(f, original_right) {
333+
let leftmost = get_leftmost_expression(original_right);
334+
f.print_leading_comments(leftmost.span())
335+
} else {
336+
None
337+
};
338+
294339
let right_document = vec![
295340
in f.arena;
296341
if operator_has_leading_comments || (line_before_operator && !should_inline_this_level) {
@@ -304,8 +349,14 @@ fn print_binaryish_expression_parts<'arena>(
304349
} else {
305350
Document::Line(if has_space_around { Line::default() } else { Line::soft() })
306351
},
307-
if should_inline_this_level {
308-
Document::Group(Group::new(vec![in f.arena; right.format(f)]))
352+
if let Some(comments) = leftmost_leading_comments {
353+
if should_inline_this_level {
354+
Document::Array(vec![in f.arena; comments, Document::Group(Group::new(vec![in f.arena; right.format(f)]))])
355+
} else {
356+
Document::Array(vec![in f.arena; comments, right.format(f)])
357+
}
358+
} else if should_inline_this_level {
359+
Document::Group(Group::new(vec![in f.arena; right.format(f)]))
309360
} else {
310361
right.format(f)
311362
},
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
if (
4+
$x &&
5+
// Condition A
6+
(
7+
$x && $x ||
8+
// Condition B
9+
$x && $x
10+
)
11+
) {
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
if ($x && ((
3+
// Condition A
4+
$x && $x
5+
) || (
6+
// Condition B
7+
$x && $x
8+
))) {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FormatSettings {
2+
..Default::default()
3+
}

crates/formatter/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ test_case!(issue_727);
307307
test_case!(issue_738);
308308
test_case!(issue_788);
309309
test_case!(issue_796);
310+
test_case!(issue_812);
310311

311312
#[test]
312313
fn test_all_test_cases_are_ran() {

0 commit comments

Comments
 (0)