Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly handle left/right breaking of binary expression #4985

Merged
merged 1 commit into from
Jun 21, 2023

Conversation

MichaReiser
Copy link
Member

@MichaReiser MichaReiser commented Jun 9, 2023

Summary

Black supports for layouts when it comes to breaking binary expressions:

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BinaryLayout {
    /// Put each operand on their own line if either side expands
    Default,

    /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
    ///
    ///```python
    /// [
    ///     a,
    ///     b
    /// ] & c
    ///```
    ExpandLeft,

    /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
    ///
    /// ```python
    /// a & [
    ///     b,
    ///     c
    /// ]
    /// ```
    ExpandRight,

    /// Both the left and right side can be expanded. Try in the following order:
    /// * expand the right side
    /// * expand the left side
    /// * expand both sides
    ///
    /// to make the expression fit
    ///
    /// ```python
    /// [
    ///     a,
    ///     b
    /// ] & [
    ///     c,
    ///     d
    /// ]
    /// ```
    ExpandRightThenLeft,
}

Our current implementation only handles ExpandRight and Default correctly. This PR adds support for ExpandRightThenLeft and ExpandLeft.

Test Plan

I added tests that play through all 4 binary expression layouts.

@MichaReiser
Copy link
Member Author

MichaReiser commented Jun 9, 2023

@MichaReiser MichaReiser mentioned this pull request Jun 9, 2023
@MichaReiser MichaReiser changed the title Fix more binop line break Correctly handle left/right breaking of binary expression Jun 9, 2023
@github-actions
Copy link
Contributor

github-actions bot commented Jun 9, 2023

PR Check Results

Ecosystem

✅ ecosystem check detected no changes.

Benchmark

Linux

group                                      main                                   pr
-----                                      ----                                   --
formatter/large/dataset.py                 1.00      6.5±0.02ms     6.3 MB/sec    1.00      6.4±0.01ms     6.3 MB/sec
formatter/numpy/ctypeslib.py               1.03   1362.0±2.58µs    12.2 MB/sec    1.00   1323.2±1.41µs    12.6 MB/sec
formatter/numpy/globals.py                 1.01    131.9±0.86µs    22.4 MB/sec    1.00    130.1±0.24µs    22.7 MB/sec
formatter/pydantic/types.py                1.02      2.7±0.01ms     9.5 MB/sec    1.00      2.6±0.02ms     9.7 MB/sec
linter/all-rules/large/dataset.py          1.00     13.1±0.04ms     3.1 MB/sec    1.00     13.1±0.04ms     3.1 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.01      3.3±0.00ms     5.0 MB/sec    1.00      3.3±0.00ms     5.0 MB/sec
linter/all-rules/numpy/globals.py          1.00    424.8±0.49µs     6.9 MB/sec    1.00    424.6±1.95µs     6.9 MB/sec
linter/all-rules/pydantic/types.py         1.00      5.8±0.01ms     4.4 MB/sec    1.00      5.8±0.01ms     4.4 MB/sec
linter/default-rules/large/dataset.py      1.01      6.7±0.02ms     6.1 MB/sec    1.00      6.6±0.01ms     6.2 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.01   1473.3±2.73µs    11.3 MB/sec    1.00   1463.3±2.23µs    11.4 MB/sec
linter/default-rules/numpy/globals.py      1.01    166.7±0.45µs    17.7 MB/sec    1.00    165.5±0.35µs    17.8 MB/sec
linter/default-rules/pydantic/types.py     1.01      3.1±0.01ms     8.3 MB/sec    1.00      3.0±0.01ms     8.4 MB/sec

Windows

group                                      main                                    pr
-----                                      ----                                    --
formatter/large/dataset.py                 1.00     11.1±0.85ms     3.7 MB/sec     1.03     11.3±0.54ms     3.6 MB/sec
formatter/numpy/ctypeslib.py               1.00  1938.6±224.37µs     8.6 MB/sec    1.09      2.1±0.05ms     7.9 MB/sec
formatter/numpy/globals.py                 1.00   177.0±11.00µs    16.7 MB/sec     1.30   229.6±29.17µs    12.9 MB/sec
formatter/pydantic/types.py                1.00      3.8±0.44ms     6.8 MB/sec     1.15      4.3±0.22ms     5.9 MB/sec
linter/all-rules/large/dataset.py          1.14     20.3±1.57ms     2.0 MB/sec     1.00     17.8±1.01ms     2.3 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.18      5.4±0.51ms     3.1 MB/sec     1.00      4.6±0.23ms     3.7 MB/sec
linter/all-rules/numpy/globals.py          1.00   654.3±38.63µs     4.5 MB/sec     1.03   677.1±27.34µs     4.4 MB/sec
linter/all-rules/pydantic/types.py         1.11      8.8±0.40ms     2.9 MB/sec     1.00      7.9±0.62ms     3.2 MB/sec
linter/default-rules/large/dataset.py      1.00     11.2±0.74ms     3.6 MB/sec     1.04     11.6±0.52ms     3.5 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.01      2.4±0.21ms     7.0 MB/sec     1.00      2.4±0.10ms     7.1 MB/sec
linter/default-rules/numpy/globals.py      1.02   290.7±24.63µs    10.1 MB/sec     1.00   285.0±17.19µs    10.4 MB/sec
linter/default-rules/pydantic/types.py     1.00      5.0±0.29ms     5.1 MB/sec     1.01      5.0±0.23ms     5.1 MB/sec

Comment on lines 46 to 75
BinaryLayout::Default => {
let comments = f.context().comments().clone();
let operator_comments = comments.dangling_comments(item.as_any_node_ref());
let needs_space = !is_simple_power_expression(item);

let before_operator_space = if needs_space {
soft_line_break_or_space()
} else {
soft_line_break()
};

write!(
f,
[
left.format(),
before_operator_space,
op.format(),
trailing_comments(operator_comments),
]
)?;

// Format the operator on its own line if the right side has any leading comments.
if comments.has_leading_comments(right.as_ref()) {
write!(f, [hard_line_break()])?;
} else if needs_space {
write!(f, [space()])?;
}

write!(f, [group(&right.format())])
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged, moved up

Comment on lines 128 to 141
write!(
f,
[
// Wrap the left in a group and gives it an id. The printer first breaks the
// right side if `right` contains any line break because the printer breaks
// sequences of groups from right to left.
// Indents the left side if the group breaks.
group(&format_args![
if_group_breaks(&text("(")),
indent_if_group_breaks(
&format_args![
soft_line_break(),
left.format(),
soft_line_break_or_space(),
op.format(),
space()
],
left_group
)
])
.with_group_id(Some(left_group)),
// Wrap the right in a group and indents its content but only if the left side breaks
group(&indent_if_group_breaks(&right.format(), left_group)),
// If the left side breaks, insert a hard line break to finish the indent and close the open paren.
if_group_breaks(&format_args![hard_line_break(), text(")")])
.with_group_id(Some(left_group))
]
)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged

@MichaReiser MichaReiser added internal An internal refactor or improvement formatter Related to the formatter labels Jun 19, 2023
@MichaReiser MichaReiser force-pushed the more-bin-op-formatting branch 2 times, most recently from 439c585 to a39dded Compare June 19, 2023 15:48
@MichaReiser MichaReiser changed the base branch from main to best-fitting-mode June 19, 2023 15:55
@MichaReiser MichaReiser mentioned this pull request Jun 19, 2023
@MichaReiser MichaReiser requested a review from konstin June 19, 2023 15:56
@MichaReiser MichaReiser marked this pull request as ready for review June 19, 2023 15:57
@MichaReiser MichaReiser changed the base branch from best-fitting-mode to comments-any-into-AnyNodeRef June 20, 2023 07:45
@MichaReiser MichaReiser force-pushed the comments-any-into-AnyNodeRef branch 2 times, most recently from d6272b0 to d90460d Compare June 20, 2023 16:34
crates/ruff_python_formatter/src/expression/expr_bin_op.rs Outdated Show resolved Hide resolved
text("("),
block_indent(&format_args![
left,
hard_line_break(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting always looks kinda odd to me but i guess python doesn't allow much better

// Wrap the left in a group and gives it an id. The printer first breaks the
// right side if `right` contains any line break because the printer breaks
// sequences of groups from right to left.
// Indents the left side if the group breaks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm having trouble following this code, shouldn't right breaks and if right is expanded but this is insufficient the whole thing should be parenthesized and indented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure if I understand this comment.

The formatter measures the left group to see if it fits. It does that by measuring if all the content up to the first line break (soft or hard) in the right group (or whatever comes after) fits on a line. It doesn't expand the left group if that's the case.

This is is the behavior that we want. We want to expand the right first and not add parentheses in that case. We only want to add parentheses and indent the code if the left expands. That's why indent_if_group_breaks and the if_group_breaks refer to the left group and not their enclosing group.

Base automatically changed from comments-any-into-AnyNodeRef to main June 20, 2023 16:49
@MichaReiser MichaReiser merged commit 3973836 into main Jun 21, 2023
@MichaReiser MichaReiser deleted the more-bin-op-formatting branch June 21, 2023 07:40
@MichaReiser
Copy link
Member Author

@MichaReiser merged this pull request with Graphite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
formatter Related to the formatter internal An internal refactor or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants