-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Fix panic when formatting comments in unary expressions #21410
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
base: main
Are you sure you want to change the base?
Conversation
Summary -- This is a second attempt at fixing #19226 based on the feedback in #20494 (comment). We currently mark the comment in an expression like this: ```py if '' and (not # 0): pass ``` as a leading comment on `not`, eventually causing a panic in `Operand::has_unparenthesized_leading_comments` because the end of the comment is greater than the start of the expression. https://github.com/astral-sh/ruff/blob/a1d9cb5830eca9b63e7fb529504fc536e99bca23/crates/ruff_python_formatter/src/expression/binary_like.rs#L843 This PR fixes the issue by instead making such a comment a dangling comment on the unary expression. In the third commit, I instead tried making the comment a leading comment on the operand, which also looks pretty reasonable to me. Making it a dangling comment seems more in line with the docs on `handle_unary_op_comments`, though. I also tried deleting the leading comment logic in favor of the new dangling logic in the fifth commit before reverting in the sixth. This looks okay to me too, but the current state of the PR seems like the least invasive fix. Test Plan -- A new, minimized test case based on the issue. I also checked that the original snippet from the report works now. Co-authored-by: Takayuki Maeda <takoyaki0316@gmail.com>
fix leading comments
This reverts commit 4c8540ede798f544a895225b541f46552649fd7b.
|
| if comment.end() < up_to { | ||
| .map(|lparen| lparen.start()); | ||
| let up_to = lparen_start.unwrap_or(unary_op.operand.start()); | ||
| if lparen_start.is_none() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a test case for
if '' and (not #
(0)
):and
if '' and (not
( #
0
)):There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well that immediately panicked again, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second case is okay, it formats like this:
if "" and (
not ( #
0
)
):
passbut the first case is related to an existing test case:
x = (
# a
not # b
# c
( # d
# e
True
)
)which formats like this:
x = (
# a
# b
# c
not ( # d
# e
True
)
)but this causes a panic on main:
x = '' and (
# a
not # b
# c
( # d
# e
True
)
)So I'm kind of wondering if adjusting the placement is really the right fix. It seems like we often want this kind of comment to become a leading comment to preserve the relative order with other comments and a change to the parent expression is what introduces the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried a different fix in binary_like in a separate branch: main...brent/unary-comment2
I kind of like this because it feels more targeted and doesn't affect any existing snapshots, but it also feels a bit like just treating the symptom. However, it seems hard to differentiate the cases in my comment above otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I guess that branch is quite similar to #20494 though 😅
| && comment.end() < unary_op.operand.start() | ||
| && comment.line_position().is_end_of_line() | ||
| { | ||
| CommentPlacement::dangling(unary_op, comment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we now need to update the documentation of this method and explain in which situation it makes comments as dangling
| if ( # comment | ||
| not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ||
| if ( | ||
| not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you double check that
if ( # comment
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Comment
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
passstill formats the same before and after your PR, just to make sure we don't accidentially change the stable style
crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap
Outdated
Show resolved
Hide resolved
|
I played a bit more with this and chatted with Micha, and I think we should just patch this in the binary expression formatting. I think the closest I got to working something out was making the comment placement handling in ruff/crates/ruff_python_formatter/src/comments/placement.rs Lines 1867 to 1870 in 3d4b055
ruff/crates/ruff_python_formatter/src/expression/expr_unary_op.rs Lines 35 to 40 in ddcff38
But this causes issues with other parent expression formatting. For example, the if (
not
# comment
a):
passwhich was formatted to if
# comment
not a:
passI also temporarily added a check for leading comments that start after their expression in I still need to verify that the surrounding code makes sense after a change like in main...brent/unary-comment2, but that's what I'm currently planning to pursue. |
|
Regarding the |
Ooh, thank you! This is looking quite promising. I'm getting an extra newline now, but if I can fix that, this might work! 361 378 │ not a
362 379 │ ):
363 380 │ pass
364 381 │
365 │-if ( # comment
382 │+if (
383 │+ # comment
366 384 │ not a
367 385 │ ):
368 386 │ pass
369 387 │ |
|
Tracking down that newline turned out to be quite the adventure. I think this is another case where things "just work" if the comments on the unary op are leading but would require additional special-casing as dangling comments. I'm pushing up my changes from the other branch with a bit of refactoring and some documentation, but I'm happy to revisit if I'm still missing something. |
Can you share an example of where it goes wrong? |
|
These cases all have their comments moved from end of line to own-line comments, as in the diff above: if (
not # comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
not # comment
a):
pass
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
passI tracked the issue into
triggered by not having leading comments here: ruff/crates/ruff_python_formatter/src/expression/mod.rs Lines 127 to 130 in d63b4b0
And I was playing with this approach on yet another branch full of debug prints, in case you want to try it: main...brent/unary-3 |
see #21410 (comment), but the short summary is that `if` (and likely other) statement formatting code that uses `maybe_parenthesize` checks if the condition has any leading or trailing comments, so if we try to smuggle the comments in as dangling comments, it thinks the expression won't break, so it doesn't add parentheses when formatting a case like this: ```py if ( not # comment a): pass ``` and we end up with a syntax error: ```py if not a: pass ``` There may be some other way around this, but this is why I'm giving up for now. It really feels like we want another CommentPlacement variant or some kind of dangling tag, like Micha mentioned when I met with him.
|
I'm not sure I follow your latest changes. Pasting your snippet shows that all these comments are places as leading unary expression comments and not dangling comments. Is this intentional? You can see this when opening the comments tab in the playground |
|
Yes that's intentional. I reverted my earlier changes where I was trying to make them dangling comments, restored them to leading comments like on Basically I gave up on the initial approach, but I'm still happy to keep trying if we come up with something better. |
|
Oh I see. I don't think I understand the changes you made in that branch. What I'd expected:
Happy to jump on a pair programming on this but I think we should maybe start fresh in the unary expression formatting and try to make small incremental changes. |
I thought this is what we wanted to avoid splitting the operator and the operand?
If I'm thinking of the same hard line break, this would change our stable formatting. That's what I meant about the newline in my comments above. But maybe you're thinking of a different one.
That sounds good! The other branch was my attempt to start fresh, but I'm sure I missed a simpler option along the way. I'll convert this back to a draft for now. |
Summary
This is a second attempt that fixes #19226 based on the feedback in #20494 (comment).
We currently mark the comment in an expression like this:
as a leading comment on
not, eventually causing a panic inOperand::has_unparenthesized_leading_commentsbecause the end of the commentis greater than the start of the expression.
ruff/crates/ruff_python_formatter/src/expression/binary_like.rs
Line 843 in a1d9cb5
This PR fixes the issue by instead making such a comment a dangling comment on
the unary expression.
In the third commit, I instead tried making the comment a leading comment on the
operand, which also looks pretty reasonable to me. Making it a dangling comment
seems more in line with the docs on
handle_unary_op_comments, though.I also tried deleting the leading comment logic in favor of the new dangling
logic in the fifth commit before reverting in the sixth. This looks okay to me
too, but the current state of the PR seems like the least invasive fix.
Test Plan
A new, minimized test case based on the issue. I also checked that the original
snippet from the report works now.