Skip to content

Commit d59024d

Browse files
committed
fix(formatter): align binary operator formatting with PER standard
This commit updates the default formatting for binary operators to align with the PER Coding Style standard and fixes several related indentation bugs that this change revealed. The default value for `line_before_binary_operator` has now been set to `true`. This enforces the modern style where the operator is placed at the beginning of the new line in multi-line expressions. ```php // Old Default if ( $longName && $anotherLongName ) { // ... } // New Default if ( $longName && $anotherLongName ) { // ... } ```` Changing this default uncovered issues where the formatter would apply an extra, incorrect level of indentation to the line containing the operator. The logic has been corrected to remove this extra indent, ensuring that chained conditions in conditions, assignments, and return statements are formatted cleanly and according to the PER standard. Signed-off-by: azjezz <[email protected]>
1 parent d15396e commit d59024d

File tree

25 files changed

+230
-136
lines changed

25 files changed

+230
-136
lines changed

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

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,19 @@ pub(super) fn print_binaryish_expression<'arena>(
2929

3030
let grandparent = f.grandparent_node();
3131

32-
let is_inside_parenthesis = matches!(
33-
grandparent,
34-
Some(
35-
Node::If(_)
36-
| Node::IfStatementBodyElseIfClause(_)
37-
| Node::IfColonDelimitedBodyElseIfClause(_)
38-
| Node::While(_)
39-
| Node::Switch(_)
40-
| Node::DoWhile(_)
41-
| Node::Match(_)
42-
)
43-
);
32+
let is_inside_parenthesis = f.is_wrapped_in_parens
33+
|| matches!(
34+
grandparent,
35+
Some(
36+
Node::If(_)
37+
| Node::IfStatementBodyElseIfClause(_)
38+
| Node::IfColonDelimitedBodyElseIfClause(_)
39+
| Node::While(_)
40+
| Node::Switch(_)
41+
| Node::DoWhile(_)
42+
| Node::Match(_)
43+
)
44+
);
4445

4546
let parts = print_binaryish_expressions(f, left, operator, right, is_inside_parenthesis, false);
4647

@@ -55,7 +56,7 @@ pub(super) fn print_binaryish_expression<'arena>(
5556
// $this->lookahead()->type === $tt->parenLeft
5657
// ) {
5758
if is_inside_parenthesis {
58-
return Document::Indent(parts);
59+
return Document::Array(parts);
5960
}
6061

6162
// Break between the parens in
@@ -74,12 +75,17 @@ pub(super) fn print_binaryish_expression<'arena>(
7475
]));
7576
}
7677

77-
let should_not_indent = matches!(grandparent, Some(Node::Return(_) | Node::Throw(_)))
78-
|| matches!(grandparent, Some(Node::ArrowFunction(func)) if func.arrow.is_before(operator.span()))
79-
|| matches!(grandparent, Some(Node::For(r#for)) if r#for.body.span().is_after(operator.span()))
80-
|| (matches!(grandparent, Some(Node::Conditional(_)))
81-
&& !matches!(f.great_grandparent_node(), Some(Node::Return(_) | Node::Throw(_)))
82-
&& !is_at_call_like_expression(f));
78+
let should_not_indent = if let Some(Node::Binary(parent_binary)) = grandparent {
79+
(parent_binary.operator.is_comparison() && operator.is_comparison())
80+
|| (parent_binary.operator.is_logical() && operator.is_logical())
81+
} else {
82+
matches!(grandparent, Some(Node::Return(_) | Node::Throw(_)))
83+
|| matches!(grandparent, Some(Node::ArrowFunction(func)) if func.arrow.is_before(operator.span()))
84+
|| matches!(grandparent, Some(Node::For(r#for)) if r#for.body.span().is_after(operator.span()))
85+
|| (matches!(grandparent, Some(Node::Conditional(_)))
86+
&& !matches!(f.great_grandparent_node(), Some(Node::Return(_) | Node::Throw(_)))
87+
&& !is_at_call_like_expression(f))
88+
};
8389

8490
let should_indent_if_inlining =
8591
matches!(grandparent, Some(Node::Assignment(_) | Node::PropertyItem(_) | Node::ConstantItem(_)))
@@ -197,6 +203,7 @@ pub(super) fn should_inline_binary_expression<'arena>(expression: &'arena Expres
197203

198204
pub(super) fn should_inline_binary_rhs_expression(rhs: &Expression, operator: &BinaryOperator) -> bool {
199205
match unwrap_parenthesized(rhs) {
206+
Expression::Assignment(_) => true,
200207
Expression::Array(Array { elements, .. })
201208
| Expression::List(List { elements, .. })
202209
| Expression::LegacyArray(LegacyArray { elements, .. }) => {

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,35 @@ pub fn format_return_value<'arena>(
2929
}
3030

3131
if return_argument_has_leading_comment(f, value) {
32+
let was_inside_parens = f.is_wrapped_in_parens;
33+
f.is_wrapped_in_parens = true;
34+
let value_doc = value.format(f);
35+
f.is_wrapped_in_parens = was_inside_parens;
36+
3237
return Document::Array(vec![
3338
in f.arena;
3439
(Document::String("(")),
35-
(Document::Indent(vec![in f.arena; Document::Line(Line::hard()), value.format(f)])),
40+
(Document::Indent(vec![in f.arena; Document::Line(Line::hard()), value_doc])),
3641
(Document::Line(Line::hard())),
3742
(Document::String(")")),
3843
]);
3944
}
4045

4146
match value {
4247
Expression::Binary(binary)
43-
if (!is_simple_expression(binary.lhs) && !is_simple_expression(binary.rhs))
48+
if (!is_simple_expression(binary.lhs) || !is_simple_expression(binary.rhs))
4449
|| (binary.lhs.is_binary() || binary.rhs.is_binary()) =>
4550
{
51+
let was_inside_parens = f.is_wrapped_in_parens;
52+
f.is_wrapped_in_parens = true;
53+
let value_doc = value.format(f);
54+
f.is_wrapped_in_parens = was_inside_parens;
55+
4656
Document::Group(Group::new(vec![
4757
in f.arena;
4858
Document::IfBreak(IfBreak::then(f.arena, Document::String("("))),
4959
Document::IndentIfBreak(IndentIfBreak::new(
50-
vec![in f.arena; Document::Line(Line::soft()), value.format(f)],
60+
vec![in f.arena; Document::Line(Line::soft()), value_doc],
5161
)),
5262
Document::Line(Line::soft()),
5363
Document::IfBreak(IfBreak::then(f.arena, Document::String(")"))),
@@ -58,11 +68,16 @@ pub fn format_return_value<'arena>(
5868
|| (matches!(conditional.then.as_ref(), Some(Expression::Conditional(_)))
5969
&& matches!(conditional.r#else, Expression::Conditional(_))) =>
6070
{
71+
let was_inside_parens = f.is_wrapped_in_parens;
72+
f.is_wrapped_in_parens = true;
73+
let value_doc = value.format(f);
74+
f.is_wrapped_in_parens = was_inside_parens;
75+
6176
Document::Group(Group::new(vec![
6277
in f.arena;
6378
Document::IfBreak(IfBreak::then(f.arena, Document::String("("))),
6479
Document::IndentIfBreak(IndentIfBreak::new(
65-
vec![in f.arena; Document::Line(Line::soft()), value.format(f)],
80+
vec![in f.arena; Document::Line(Line::soft()), value_doc],
6681
)),
6782
Document::Line(Line::soft()),
6883
Document::IfBreak(IfBreak::then(f.arena, Document::String(")"))),

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use bumpalo::collections::Vec;
55
use bumpalo::vec;
66

77
use bumpalo::Bump;
8+
use mago_span::HasPosition;
89
use mago_span::HasSpan;
910
use mago_syntax::ast::*;
1011

@@ -214,7 +215,7 @@ fn should_add_new_line_or_space_after_stmt<'arena>(
214215
DeclareBody::ColonDelimited(_) => true,
215216
},
216217
Statement::OpeningTag(_) => {
217-
if let Some(index) = f.skip_to_line_end(Some(stmt.span().end_position().offset))
218+
if let Some(index) = f.skip_to_line_end(Some(stmt.end_position().offset()))
218219
&& f.has_newline(index, false)
219220
{
220221
return (true, false);
@@ -225,7 +226,7 @@ fn should_add_new_line_or_space_after_stmt<'arena>(
225226
false
226227
}
227228
_ => {
228-
if f.has_newline(stmt.span().end_position().offset, false) {
229+
if f.has_newline(stmt.end_position().offset(), false) {
229230
true
230231
} else if let Some(Statement::ClosingTag(_)) = stmts.get(i + 1) {
231232
should_add_space = !f.has_comment(stmt.span(), CommentFlags::Trailing);

crates/formatter/src/internal/macros.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ macro_rules! wrap {
33
($f:ident, $self:expr, $node:ident, $block:block) => {{
44
let node = mago_syntax::ast::Node::$node($self);
55
$f.enter_node(node);
6+
7+
let was_wrapped_in_parens = $f.is_wrapped_in_parens;
8+
let needed_to_wrap_in_parens = $f.need_parens(node);
9+
$f.is_wrapped_in_parens |= needed_to_wrap_in_parens;
610
let leading = $f.print_leading_comments(node.span());
711
let doc = $block;
8-
let doc = $f.wrap_parens(doc, node);
12+
let doc = if needed_to_wrap_in_parens { $f.add_parens(doc, node) } else { doc };
913
let trailing = $f.print_trailing_comments_for_node(node);
1014
let doc = $f.print_comments(leading, doc, trailing);
1115
$f.leave_node();
16+
$f.is_wrapped_in_parens = was_wrapped_in_parens;
1217
doc
1318
}};
1419
}

crates/formatter/src/internal/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub struct FormatterState<'ctx, 'arena> {
5151
in_pipe_chain_arrow_segment: bool,
5252
in_script_terminating_statement: bool,
5353
in_condition: bool,
54+
is_wrapped_in_parens: bool,
5455
halted_compilation: bool,
5556
}
5657

@@ -85,6 +86,7 @@ impl<'ctx, 'arena> FormatterState<'ctx, 'arena> {
8586
parameter_state: ParameterState::default(),
8687
in_pipe_chain_arrow_segment: false,
8788
in_condition: false,
89+
is_wrapped_in_parens: false,
8890
halted_compilation: false,
8991
in_script_terminating_statement: false,
9092
}

crates/formatter/src/internal/parens.rs

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,45 @@ use mago_syntax::token::Precedence;
88

99
use crate::document::Document;
1010
use crate::document::Group;
11+
use crate::document::IndentIfBreak;
1112
use crate::document::Line;
1213
use crate::internal::FormatterState;
1314

1415
impl<'ctx, 'arena> FormatterState<'ctx, 'arena> {
15-
pub(crate) fn wrap_parens(&mut self, document: Document<'arena>, node: Node<'arena, 'arena>) -> Document<'arena> {
16-
if self.need_parens(node) {
17-
if self.should_indent(node) {
18-
Document::Group(Group::new(vec![
16+
pub(crate) fn add_parens(&mut self, document: Document<'arena>, node: Node<'arena, 'arena>) -> Document<'arena> {
17+
if self.should_indent(node) {
18+
Document::Group(Group::new(vec![
19+
in self.arena;
20+
Document::String("("),
21+
Document::IndentIfBreak(IndentIfBreak::new(vec![
1922
in self.arena;
20-
Document::String("("),
21-
Document::Indent(vec![
22-
in self.arena;
23-
if self.settings.space_within_grouping_parenthesis {
24-
Document::Line(Line::default())
25-
} else {
26-
Document::Line(Line::soft())
27-
},
28-
document,
29-
]),
3023
if self.settings.space_within_grouping_parenthesis {
3124
Document::Line(Line::default())
3225
} else {
3326
Document::Line(Line::soft())
3427
},
35-
Document::String(")"),
36-
]))
37-
} else {
38-
Document::Group(Group::new(vec![
39-
in self.arena;
40-
Document::String("("),
41-
if self.settings.space_within_grouping_parenthesis { Document::space() } else { Document::empty() },
4228
document,
43-
if self.settings.space_within_grouping_parenthesis { Document::space() } else { Document::empty() },
44-
Document::String(")"),
45-
]))
46-
}
29+
])),
30+
if self.settings.space_within_grouping_parenthesis {
31+
Document::Line(Line::default())
32+
} else {
33+
Document::Line(Line::soft())
34+
},
35+
Document::String(")"),
36+
]))
4737
} else {
48-
document
49-
}
50-
}
51-
52-
fn should_indent(&self, node: Node<'arena, 'arena>) -> bool {
53-
if matches!(node, Node::Program(_)) || node.is_statement() {
54-
return false;
38+
Document::Group(Group::new(vec![
39+
in self.arena;
40+
Document::String("("),
41+
if self.settings.space_within_grouping_parenthesis { Document::space() } else { Document::empty() },
42+
document,
43+
if self.settings.space_within_grouping_parenthesis { Document::space() } else { Document::empty() },
44+
Document::String(")"),
45+
]))
5546
}
56-
57-
self.is_unary_or_binary_or_ternary(node)
5847
}
5948

60-
fn need_parens(&mut self, node: Node<'arena, 'arena>) -> bool {
49+
pub(crate) fn need_parens(&mut self, node: Node<'arena, 'arena>) -> bool {
6150
if matches!(node, Node::Program(_)) || node.is_statement() {
6251
return false;
6352
}
@@ -70,6 +59,14 @@ impl<'ctx, 'arena> FormatterState<'ctx, 'arena> {
7059
|| self.pipe_node_needs_parens(node)
7160
}
7261

62+
pub(crate) fn should_indent(&self, node: Node<'arena, 'arena>) -> bool {
63+
if matches!(node, Node::Program(_)) || node.is_statement() {
64+
return false;
65+
}
66+
67+
self.is_unary_or_binary_or_ternary(node)
68+
}
69+
7370
fn literal_needs_parens(&self, node: Node<'arena, 'arena>) -> bool {
7471
let Node::Literal(Literal::Integer(_) | Literal::Float(_)) = node else {
7572
return false;

crates/formatter/src/settings.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ pub struct FormatSettings {
399399
///
400400
/// Note: If the right side has a leading comment, this setting is always false.
401401
///
402-
/// Default: false
403-
#[serde(default = "default_false")]
402+
/// Default: true
403+
#[serde(default = "default_true")]
404404
pub line_before_binary_operator: bool,
405405

406406
/// Whether to always break named argument lists into multiple lines.
@@ -869,7 +869,7 @@ impl Default for FormatSettings {
869869
null_type_hint: NullTypeHint::default(),
870870
break_promoted_properties_list: true,
871871
method_chain_breaking_style: MethodChainBreakingStyle::NextLine,
872-
line_before_binary_operator: false,
872+
line_before_binary_operator: true,
873873
sort_uses: true,
874874
separate_use_types: true,
875875
expand_use_groups: true,

crates/formatter/tests/cases/arrow_return/after.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ public function something(): void
77
$command = array_find(
88
array: $this->consoleConfig->commands,
99
callback: fn(ConsoleCommand $consoleCommand) => (
10-
$consoleCommand->handler->getDeclaringClass()->getName() === $command[0] &&
11-
$consoleCommand->handler->getName() === $command[1]
10+
$consoleCommand->handler->getDeclaringClass()->getName() === $command[0]
11+
&& $consoleCommand->handler->getName() === $command[1]
1212
),
1313
);
1414

crates/formatter/tests/cases/assignments/after.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,21 @@ function foo()
5959
{
6060
if ($write) {
6161
$writable =
62-
str_contains($meta['mode'], 'x') ||
63-
str_contains($meta['mode'], 'w') ||
64-
str_contains($meta['mode'], 'c') ||
65-
str_contains($meta['mode'], 'a') ||
66-
str_contains($meta['mode'], '+');
62+
str_contains($meta['mode'], 'x')
63+
|| str_contains($meta['mode'], 'w')
64+
|| str_contains($meta['mode'], 'c')
65+
|| str_contains($meta['mode'], 'a')
66+
|| str_contains($meta['mode'], '+');
6767
}
6868

6969
$target_directory = Env\temp_dir() . DIRECTORY_SEPARATOR . 'you-shall-not-pass';
7070

7171
if (($i + 1) < $binary_length) {
7272
$byte1 = $chunk[2];
7373
$dest .=
74-
static::encode6Bits($byte0 >> 2) .
75-
static::encode6Bits((($byte0 << 4) | ($byte1 >> 4)) & 63) .
76-
static::encode6Bits(($byte1 << 2) & 63);
74+
static::encode6Bits($byte0 >> 2)
75+
. static::encode6Bits((($byte0 << 4) | ($byte1 >> 4)) & 63)
76+
. static::encode6Bits(($byte1 << 2) & 63);
7777
}
7878

7979
$target_file =

crates/formatter/tests/cases/binary_alignment/after.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ private function addSearchClause(
2121

2222
// this complex condition is needed to avoid issues on PostgreSQL databases
2323
if (
24-
$propertyConfig['is_small_integer'] && $isSmallIntegerQueryTerm ||
25-
$propertyConfig['is_integer'] && $isIntegerQueryTerm ||
26-
$propertyConfig['is_numeric'] && $isNumericQueryTerm
24+
$propertyConfig['is_small_integer'] && $isSmallIntegerQueryTerm
25+
|| $propertyConfig['is_integer'] && $isIntegerQueryTerm
26+
|| $propertyConfig['is_numeric'] && $isNumericQueryTerm
2727
) {
2828
// ...
2929
}
@@ -34,14 +34,14 @@ private function addSearchClause(
3434
private function isValidBoolean(mixed $value): bool
3535
{
3636
return (
37-
$value === false ||
38-
$value === 'false' ||
39-
$value === 0 ||
40-
$value === '0' ||
41-
$value === true ||
42-
$value === 'true' ||
43-
$value === 1 ||
44-
$value === '1'
37+
$value === false
38+
|| $value === 'false'
39+
|| $value === 0
40+
|| $value === '0'
41+
|| $value === true
42+
|| $value === 'true'
43+
|| $value === 1
44+
|| $value === '1'
4545
);
4646
}
4747
}

0 commit comments

Comments
 (0)