diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py index 063ef5fd96..a6afdc242f 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py @@ -294,3 +294,39 @@ if parent_body: # d # e #f + +# Compare behavior with `while`/`else` comment placement + +if True: pass + # 1 +else: + pass + +if True: + pass + # 2 +else: + pass + +if True: pass +# 3 +else: + pass + +if True: pass + # 4 +else: + pass + +def foo(): + if True: + pass +# 5 + else: + pass + +if True: + first;second + # 6 +else: + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py index a09f26b550..ebe9922922 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py @@ -28,3 +28,37 @@ while ( and anotherCondition or aThirdCondition # trailing third condition ): # comment print("Do something") + +while True: pass + # 1 +else: + pass + +while True: + pass + # 2 +else: + pass + +while True: pass +# 3 +else: + pass + +while True: pass + # 4 +else: + pass + +def foo(): + while True: + pass +# 5 + else: + pass + +while True: + first;second + # 6 +else: + pass diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 81a1b4bf25..07fc789f34 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -1042,4 +1042,33 @@ else: # trailing comment assert_debug_snapshot!(comments.debug(test_case.source_code)); } + + #[test] + fn while_else_indented_comment_between_branches() { + let source = r"while True: pass + # comment +else: + pass +"; + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } + + #[test] + fn while_else_very_indented_comment_between_branches() { + let source = r"while True: + pass + # comment +else: + pass +"; + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 234752ba53..2bd7402a31 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -8,7 +8,7 @@ use ruff_python_trivia::{ find_only_token_in_range, first_non_trivia_token, indentation_at_offset, }; use ruff_source_file::LineRanges; -use ruff_text_size::{Ranged, TextLen, TextRange}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use std::cmp::Ordering; use crate::comments::visitor::{CommentPlacement, DecoratedComment}; @@ -602,9 +602,35 @@ fn handle_own_line_comment_between_branches<'a>( // following branch or if it a trailing comment of the previous body's last statement. let comment_indentation = comment_indentation_after(preceding, comment.range(), source); - let preceding_indentation = indentation(source, &preceding) - .unwrap_or_default() - .text_len(); + let preceding_indentation = indentation(source, &preceding).map_or_else( + // If `indentation` returns `None`, then there is leading + // content before the preceding node. In this case, we + // always treat the comment as being less-indented than the + // preceding. For example: + // + // ```python + // if True: pass + // # leading on `else` + // else: + // pass + // ``` + // Note we even do this if the comment is very indented + // (which matches `black`'s behavior as of 2025.11.11) + // + // ```python + // if True: pass + // # leading on `else` + // else: + // pass + // ``` + || { + comment_indentation + // This can be any positive number - we just + // want to hit the `Less` branch below + + TextSize::new(1) + }, + ruff_text_size::TextLen::text_len, + ); // Compare to the last statement in the body match comment_indentation.cmp(&preceding_indentation) { @@ -678,8 +704,41 @@ fn handle_own_line_comment_after_branch<'a>( preceding: AnyNodeRef<'a>, source: &str, ) -> CommentPlacement<'a> { - let Some(last_child) = preceding.last_child_in_body() else { - return CommentPlacement::Default(comment); + // If the preceding node has a body, we want the last child - e.g. + // + // ```python + // if True: + // def foo(): + // something + // last_child + // # comment + // else: + // pass + // ``` + // + // Otherwise, the preceding node may be the last statement in the body + // of the preceding branch, in which case we can take it as our + // `last_child` here - e.g. + // + // ```python + // if True: + // something + // last_child + // # comment + // else: + // pass + // ``` + let last_child = match preceding.last_child_in_body() { + Some(last) => last, + None if comment.following_node().is_some_and(|following| { + following.is_first_statement_in_alternate_body(comment.enclosing_node()) + }) => + { + preceding + } + _ => { + return CommentPlacement::Default(comment); + } }; // We only care about the length because indentations with mixed spaces and tabs are only valid if diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_indented_comment_between_branches.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_indented_comment_between_branches.snap new file mode 100644 index 0000000000..685f235695 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_indented_comment_between_branches.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtWhile, + range: 0..45, + source: `while True: pass⏎`, + }: { + "leading": [], + "dangling": [ + SourceComment { + text: "# comment", + position: OwnLine, + formatted: false, + }, + ], + "trailing": [], + }, +} diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_very_indented_comment_between_branches.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_very_indented_comment_between_branches.snap new file mode 100644 index 0000000000..7daeba56d1 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_else_very_indented_comment_between_branches.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtPass, + range: 16..20, + source: `pass`, + }: { + "leading": [], + "dangling": [], + "trailing": [ + SourceComment { + text: "# comment", + position: OwnLine, + formatted: false, + }, + ], + }, +} diff --git a/crates/ruff_python_formatter/tests/snapshots/format@form_feed.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@form_feed.py.snap index a461554f7d..9539965e42 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@form_feed.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@form_feed.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/form_feed.py -snapshot_kind: text --- ## Input ```python @@ -18,7 +17,7 @@ else: # Regression test for: https://github.com/astral-sh/ruff/issues/7624 if symbol is not None: request["market"] = market["id"] -# "remaining_volume": "0.0", + # "remaining_volume": "0.0", else: pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap index 7becf78515..4c561560bb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py -snapshot_kind: text --- ## Input ```python @@ -301,6 +300,42 @@ if parent_body: # d # e #f + +# Compare behavior with `while`/`else` comment placement + +if True: pass + # 1 +else: + pass + +if True: + pass + # 2 +else: + pass + +if True: pass +# 3 +else: + pass + +if True: pass + # 4 +else: + pass + +def foo(): + if True: + pass +# 5 + else: + pass + +if True: + first;second + # 6 +else: + pass ``` ## Output @@ -607,6 +642,48 @@ if parent_body: # d # e # f + +# Compare behavior with `while`/`else` comment placement + +if True: + pass +# 1 +else: + pass + +if True: + pass + # 2 +else: + pass + +if True: + pass +# 3 +else: + pass + +if True: + pass +# 4 +else: + pass + + +def foo(): + if True: + pass + # 5 + else: + pass + + +if True: + first + second +# 6 +else: + pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap index 6217656dda..c5b78fadbd 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__while.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py -snapshot_kind: text --- ## Input ```python @@ -35,6 +34,40 @@ while ( and anotherCondition or aThirdCondition # trailing third condition ): # comment print("Do something") + +while True: pass + # 1 +else: + pass + +while True: + pass + # 2 +else: + pass + +while True: pass +# 3 +else: + pass + +while True: pass + # 4 +else: + pass + +def foo(): + while True: + pass +# 5 + else: + pass + +while True: + first;second + # 6 +else: + pass ``` ## Output @@ -70,4 +103,44 @@ while ( or aThirdCondition # trailing third condition ): # comment print("Do something") + +while True: + pass +# 1 +else: + pass + +while True: + pass + # 2 +else: + pass + +while True: + pass +# 3 +else: + pass + +while True: + pass +# 4 +else: + pass + + +def foo(): + while True: + pass + # 5 + else: + pass + + +while True: + first + second +# 6 +else: + pass ```