Implement blank_line_after_nested_stub_class preview style (#9155)

## Summary

This PR implements the `blank_line_after_nested_stub_class` preview
style in the formatter.

The logic is divided into 3 parts:
1. In between preceding and following nodes at top level and nested
suite
2. When there's a trailing comment after the class
3. When there is no following node from (1) which is the case when it's
the last or the only node in a suite

We handle (3) with `FormatLeadingAlternateBranchComments`.

## Test Plan

- Add new test cases and update existing snapshots
- Checked the `typeshed` diff

fixes: #8891
This commit is contained in:
Dhruv Manilawala 2024-01-31 00:09:38 +05:30 committed by GitHub
parent 79f0522eb7
commit 541aef4e6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 891 additions and 29 deletions

View file

@ -1,5 +1,5 @@
use ruff_formatter::write;
use ruff_python_ast::{Decorator, StmtClassDef};
use ruff_python_ast::{Decorator, NodeKind, StmtClassDef};
use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia;
use ruff_text_size::Ranged;
@ -152,7 +152,10 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
//
// # comment
// ```
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtClassDef)
.fmt(f)?;
Ok(())
}
fn fmt_dangling_comments(

View file

@ -1,5 +1,5 @@
use ruff_formatter::write;
use ruff_python_ast::StmtFunctionDef;
use ruff_python_ast::{NodeKind, StmtFunctionDef};
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
@ -87,7 +87,8 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
//
// # comment
// ```
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtFunctionDef)
.fmt(f)
}
fn fmt_dangling_comments(

View file

@ -1,4 +1,6 @@
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
use ruff_formatter::{
write, FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
};
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
@ -12,8 +14,8 @@ use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, With
use crate::expression::expr_string_literal::ExprStringLiteralKind;
use crate::prelude::*;
use crate::preview::{
is_dummy_implementations_enabled, is_module_docstring_newlines_enabled,
is_no_blank_line_before_class_docstring_enabled,
is_blank_line_after_nested_stub_class_enabled, is_dummy_implementations_enabled,
is_module_docstring_newlines_enabled, is_no_blank_line_before_class_docstring_enabled,
};
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{
@ -470,17 +472,23 @@ fn stub_file_empty_lines(
let empty_line_condition = preceding_comments.has_trailing()
|| following_comments.has_leading()
|| !stub_suite_can_omit_empty_line(preceding, following, f);
let require_empty_line = should_insert_blank_line_after_class_in_stub_file(
preceding.into(),
Some(following.into()),
f.context(),
);
match kind {
SuiteKind::TopLevel => {
if empty_line_condition {
if empty_line_condition || require_empty_line {
empty_line().fmt(f)
} else {
hard_line_break().fmt(f)
}
}
SuiteKind::Class | SuiteKind::Other | SuiteKind::Function => {
if empty_line_condition
&& lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1
if (empty_line_condition
&& lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1)
|| require_empty_line
{
empty_line().fmt(f)
} else {
@ -490,6 +498,122 @@ fn stub_file_empty_lines(
}
}
/// Checks if an empty line should be inserted after a class definition.
///
/// This is only valid if the [`blank_line_after_nested_stub_class`](https://github.com/astral-sh/ruff/issues/8891)
/// preview rule is enabled and the source to be formatted is a stub file.
///
/// If `following` is `None`, then the preceding node is the last one in a suite. The
/// caller needs to make sure that the suite which the preceding node is part of is
/// followed by an alternate branch and shouldn't be a top-level suite.
pub(crate) fn should_insert_blank_line_after_class_in_stub_file(
preceding: AnyNodeRef<'_>,
following: Option<AnyNodeRef<'_>>,
context: &PyFormatContext,
) -> bool {
if !(is_blank_line_after_nested_stub_class_enabled(context)
&& context.options().source_type().is_stub())
{
return false;
}
let comments = context.comments();
match preceding.as_stmt_class_def() {
Some(class) if contains_only_an_ellipsis(&class.body, comments) => {
let Some(following) = following else {
// The formatter is at the start of an alternate branch such as
// an `else` block.
//
// ```python
// if foo:
// class Nested:
// pass
// else:
// pass
// ```
//
// In the above code, the preceding node is the `Nested` class
// which has no following node.
return true;
};
// If the preceding class has decorators, then we need to add an empty
// line even if it only contains ellipsis.
//
// ```python
// class Top:
// @decorator
// class Nested1: ...
// foo = 1
// ```
let preceding_has_decorators = !class.decorator_list.is_empty();
// If the following statement is a class definition, then an empty line
// should be inserted if it (1) doesn't just contain ellipsis, or (2) has decorators.
//
// ```python
// class Top:
// class Nested1: ...
// class Nested2:
// pass
//
// class Top:
// class Nested1: ...
// @decorator
// class Nested2: ...
// ```
//
// Both of the above examples should add a blank line in between.
let following_is_class_without_only_ellipsis_or_has_decorators =
following.as_stmt_class_def().is_some_and(|following| {
!contains_only_an_ellipsis(&following.body, comments)
|| !following.decorator_list.is_empty()
});
preceding_has_decorators
|| following_is_class_without_only_ellipsis_or_has_decorators
|| following.is_stmt_function_def()
}
Some(_) => {
// Preceding statement is a class definition whose body isn't only an ellipsis.
// Here, we should only add a blank line if the class doesn't have a trailing
// own line comment as that's handled by the class formatting itself.
!comments.has_trailing_own_line(preceding)
}
None => {
// If preceding isn't a class definition, let's check if the last statement
// in the body, going all the way down, is a class definition.
//
// ```python
// if foo:
// if bar:
// class Nested:
// pass
// if other:
// pass
// ```
//
// But, if it contained a trailing own line comment, then it's handled
// by the class formatting itself.
//
// ```python
// if foo:
// if bar:
// class Nested:
// pass
// # comment
// if other:
// pass
// ```
std::iter::successors(
preceding.last_child_in_body(),
AnyNodeRef::last_child_in_body,
)
.take_while(|last_child| !comments.has_trailing_own_line(*last_child))
.any(|last_child| last_child.is_stmt_class_def())
}
}
}
/// Only a function to compute it lazily
fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyFormatter) -> bool {
// Two subsequent class definitions that both have an ellipsis only body