Skip to content

Conversation

@TaKO8Ki
Copy link
Contributor

@TaKO8Ki TaKO8Ki commented Sep 21, 2025

Summary

Fixes #19226

Fixes a panic in binary expression formatting caused by TextRange assertion failures when comments are positioned between unary operators (like not) and their operands.

The issue occurred because expression.start() points to the unary operator while comment.end() can be positioned after it, creating invalid TextRange where start > end. The fix uses operand.start() for unary operations to ensure proper ordering.

Test Plan

I have added new test cases to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py.

@github-actions
Copy link
Contributor

ruff-ecosystem results

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@MichaReiser
Copy link
Member

Thank you. I need to think about whether this is the intended comment placement as it means that a comment can now move "forward" (it moves from after not to before not)

@MichaReiser MichaReiser added bug Something isn't working formatter Related to the formatter labels Sep 23, 2025
@TaKO8Ki
Copy link
Contributor Author

TaKO8Ki commented Sep 23, 2025

@MichaReiser Ok. If it's not intentional, I can make has_lparen_between_comment_and_expression to skip unary.

@MichaReiser
Copy link
Member

Sorry for the late reply.

I just took a look at which node the formatter associates the comment for:

if '' and (not #
0):
    pass
{
    Node {
        kind: ExprUnaryOp,
        range: 11..18,
        source: `not #⏎`,
    }: {
        "leading": [
            SourceComment {
                text: "#",
                position: EndOfLine,
                formatted: false,
            },
        ],
        "dangling": [],
        "trailing": [],
    },
}

To me, associating the comment as the leading comment of not seems incorrect, as it doesn't come before the unary not.

I'd either expect this to be a leading comment of 0, or a trailing comment of the unary expression.

An other alternative, and I think my preferred solution, is to make the comment a dangling comment of the unary expression instead. In fact, it seems we already have some handling for unary comments inside place_comment. Maybe all that is needed is to make that work for the given case?

fn handle_unary_op_comment<'a>(
comment: DecoratedComment<'a>,
unary_op: &'a ast::ExprUnaryOp,
source: &str,
) -> CommentPlacement<'a> {
let mut tokenizer = SimpleTokenizer::new(
source,
TextRange::new(unary_op.start(), unary_op.operand.start()),
)
.skip_trivia();
let op_token = tokenizer.next();
debug_assert!(op_token.is_some_and(|token| matches!(
token.kind,
SimpleTokenKind::Tilde
| SimpleTokenKind::Not
| SimpleTokenKind::Plus
| SimpleTokenKind::Minus
)));
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to {
CommentPlacement::leading(unary_op, comment)
} else {
CommentPlacement::Default(comment)
}
}

ntBre added a commit that referenced this pull request Nov 12, 2025
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working formatter Related to the formatter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Panic assertion failed: start.raw <= end.raw in ruff_python_formatter/src/expression/binary_like.rs

2 participants