Try statements have a body: Fix formatter instability (#5558)

## Summary

The following code was previously leading to unstable formatting:
```python
try:
    try:
        pass
    finally:
        print(1)  # issue7208
except A:
    pass
```
The comment would be formatted as a trailing comment of `try` which is
unstable as an end-of-line comment gets two extra whitespaces.

This was originally found in
99b00efd5e/Lib/getpass.py (L68-L91)

## Test Plan

I added a regression test
This commit is contained in:
konsti 2023-07-06 16:07:47 +02:00 committed by GitHub
parent 25981420c4
commit 8184235f93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 88 additions and 76 deletions

View file

@ -4281,6 +4281,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtFunctionDef(_)
| AnyNodeRef::StmtAsyncFunctionDef(_) | AnyNodeRef::StmtAsyncFunctionDef(_)
| AnyNodeRef::StmtClassDef(_) | AnyNodeRef::StmtClassDef(_)
| AnyNodeRef::StmtTry(_)
| AnyNodeRef::StmtTryStar(_)
) )
} }
} }

View file

@ -89,3 +89,13 @@ else:
# before finally # before finally
finally: finally:
... ...
# try and try star are statements with body
# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91
try:
try:
pass
finally:
print(1) # issue7208
except A:
pass

View file

@ -335,89 +335,89 @@ fn handle_in_between_bodies_end_of_line_comment<'a>(
} }
// The comment must be between two statements... // The comment must be between two statements...
if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node())
{ else {
// ...and the following statement must be the first statement in an alternate body of the parent... return CommentPlacement::Default(comment);
if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { };
// ```python
// if test:
// a
// # comment
// b
// ```
return CommentPlacement::Default(comment);
}
if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { // ...and the following statement must be the first statement in an alternate body of the parent...
// The `elif` or except handlers have their own body to which we can attach the trailing comment if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) {
// ```python
// if test:
// a
// # comment
// b
// ```
return CommentPlacement::Default(comment);
}
if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) {
// The `elif` or except handlers have their own body to which we can attach the trailing comment
// ```python
// if test:
// a
// elif c: # comment
// b
// ```
if following.is_except_handler() {
return CommentPlacement::trailing(following, comment);
} else if following.is_stmt_if() {
// We have to exclude for following if statements that are not elif by checking the
// indentation
// ```python // ```python
// if test: // if True:
// a // pass
// elif c: # comment // else: # Comment
// b // if False:
// ``` // pass
if following.is_except_handler() {
return CommentPlacement::trailing(following, comment);
} else if following.is_stmt_if() {
// We have to exclude for following if statements that are not elif by checking the
// indentation
// ```python
// if True:
// pass
// else: # Comment
// if False:
// pass
// pass
// ```
let base_if_indent =
whitespace::indentation_at_offset(locator, following.range().start());
let maybe_elif_indent = whitespace::indentation_at_offset(
locator,
comment.enclosing_node().range().start(),
);
if base_if_indent == maybe_elif_indent {
return CommentPlacement::trailing(following, comment);
}
}
// There are no bodies for the "else" branch and other bodies that are represented as a `Vec<Stmt>`.
// This means, there's no good place to attach the comments to.
// Make this a dangling comments and manually format the comment in
// in the enclosing node's formatting logic. For `try`, it's the formatters responsibility
// to correctly identify the comments for the `finally` and `orelse` block by looking
// at the comment's range.
//
// ```python
// while x == y:
// pass // pass
// else: # trailing
// print("nooop")
// ``` // ```
CommentPlacement::dangling(comment.enclosing_node(), comment) let base_if_indent =
} else { whitespace::indentation_at_offset(locator, following.range().start());
// Trailing comment of the preceding statement let maybe_elif_indent = whitespace::indentation_at_offset(
// ```python locator,
// while test: comment.enclosing_node().range().start(),
// a # comment );
// else: if base_if_indent == maybe_elif_indent {
// b return CommentPlacement::trailing(following, comment);
// ```
if preceding.is_node_with_body() {
// We can't set this as a trailing comment of the function declaration because it
// will then move behind the function block instead of sticking with the pass
// ```python
// if True:
// def f():
// pass # a
// else:
// pass
// ```
CommentPlacement::Default(comment)
} else {
CommentPlacement::trailing(preceding, comment)
} }
} }
// There are no bodies for the "else" branch and other bodies that are represented as a `Vec<Stmt>`.
// This means, there's no good place to attach the comments to.
// Make this a dangling comments and manually format the comment in
// in the enclosing node's formatting logic. For `try`, it's the formatters responsibility
// to correctly identify the comments for the `finally` and `orelse` block by looking
// at the comment's range.
//
// ```python
// while x == y:
// pass
// else: # trailing
// print("nooop")
// ```
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else { } else {
CommentPlacement::Default(comment) // Trailing comment of the preceding statement
// ```python
// while test:
// a # comment
// else:
// b
// ```
if preceding.is_node_with_body() {
// We can't set this as a trailing comment of the function declaration because it
// will then move behind the function block instead of sticking with the pass
// ```python
// if True:
// def f():
// pass # a
// else:
// pass
// ```
CommentPlacement::Default(comment)
} else {
CommentPlacement::trailing(preceding, comment)
}
} }
} }