Insert newline after nested function or class statements (#7946)

**Summary** Insert a newline after nested function and class
definitions, unless there is a trailing own line comment.

We need to e.g. format
```python
if platform.system() == "Linux":
    if sys.version > (3, 10):
        def f():
            print("old")
    else:
        def f():
            print("new")
    f()
```
as
```python
if platform.system() == "Linux":
    if sys.version > (3, 10):

        def f():
            print("old")

    else:

        def f():
            print("new")

    f()
```
even though `f()` is directly preceded by an if statement, not a
function or class definition. See the comments and fixtures for trailing
own line comment handling.

**Test Plan** I checked that the new content of `newlines.py` matches
black's formatting.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
konsti 2023-10-18 11:45:58 +02:00 committed by GitHub
parent dda4ceda71
commit 0c3123e07e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 430 additions and 77 deletions

View file

@ -155,13 +155,65 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
while let Some(following) = iter.next() {
let following_comments = comments.leading_dangling_trailing(following);
let needs_empty_lines = if is_class_or_function_definition(following) {
// Here we insert empty lines even if the preceding has a trailing own line comment
true
} else {
// Find nested class or function definitions that need an empty line after them.
//
// ```python
// def f():
// if True:
//
// def double(s):
// return s + s
//
// print("below function")
// ```
std::iter::successors(
Some(AnyNodeRef::from(preceding)),
AnyNodeRef::last_child_in_body,
)
.take_while(|last_child|
// If there is a comment between preceding and following the empty lines were
// inserted before the comment by preceding and there are no extra empty lines
// after the comment.
// ```python
// class Test:
// def a(self):
// pass
// # trailing comment
//
//
// # two lines before, one line after
//
// c = 30
// ````
// This also includes nested class/function definitions, so we stop recursing
// once we see a node with a trailing own line comment:
// ```python
// def f():
// if True:
//
// def double(s):
// return s + s
//
// # nested trailing own line comment
// print("below function with trailing own line comment")
// ```
!comments.has_trailing_own_line(*last_child))
.any(|last_child| {
matches!(
last_child,
AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::StmtClassDef(_)
)
})
};
// Add empty lines before and after a function or class definition. If the preceding
// node is a function or class, and contains trailing comments, then the statement
// itself will add the requisite empty lines when formatting its comments.
if (is_class_or_function_definition(preceding)
&& !preceding_comments.has_trailing_own_line())
|| is_class_or_function_definition(following)
{
if needs_empty_lines {
if source_type.is_stub() {
stub_file_empty_lines(
self.kind,