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,8 +1,7 @@
use std::borrow::Cow;
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
use ruff_python_ast::PySourceType;
use ruff_python_ast::{AnyNodeRef, AstNode};
use ruff_python_ast::{AnyNodeRef, AstNode, NodeKind, PySourceType};
use ruff_python_trivia::{
is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before,
};
@ -11,6 +10,8 @@ use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::comments::{CommentLinePosition, SourceComment};
use crate::context::NodeLevel;
use crate::prelude::*;
use crate::preview::is_blank_line_after_nested_stub_class_enabled;
use crate::statement::suite::should_insert_blank_line_after_class_in_stub_file;
/// Formats the leading comments of a node.
pub(crate) fn leading_node_comments<T>(node: &T) -> FormatLeadingComments
@ -85,7 +86,11 @@ pub(crate) struct FormatLeadingAlternateBranchComments<'a> {
impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
if let Some(first_leading) = self.comments.first() {
if self.last_node.map_or(false, |preceding| {
should_insert_blank_line_after_class_in_stub_file(preceding, None, f.context())
}) {
write!(f, [empty_line(), leading_comments(self.comments)])?;
} else if let Some(first_leading) = self.comments.first() {
// Leading comments only preserves the lines after the comment but not before.
// Insert the necessary lines.
write!(
@ -513,14 +518,32 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
/// ```
///
/// This builder will insert two empty lines before the comment.
///
/// # Preview
///
/// For preview style, this builder will insert a single empty line after a
/// class definition in a stub file.
///
/// For example, given:
/// ```python
/// class Foo:
/// pass
/// # comment
/// ```
///
/// This builder will insert a single empty line before the comment.
pub(crate) fn empty_lines_before_trailing_comments<'a>(
f: &PyFormatter,
comments: &'a [SourceComment],
node_kind: NodeKind,
) -> FormatEmptyLinesBeforeTrailingComments<'a> {
// Black has different rules for stub vs. non-stub and top level vs. indented
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
(PySourceType::Stub, _) => 0,
(PySourceType::Stub, _) => u32::from(
is_blank_line_after_nested_stub_class_enabled(f.context())
&& node_kind == NodeKind::StmtClassDef,
),
(_, NodeLevel::TopLevel(_)) => 2,
(_, _) => 1,
};

View file

@ -48,6 +48,13 @@ pub(crate) const fn is_wrap_multiple_context_managers_in_parens_enabled(
context.is_preview()
}
/// Returns `true` if the [`blank_line_after_nested_stub_class`](https://github.com/astral-sh/ruff/issues/8891) preview style is enabled.
pub(crate) const fn is_blank_line_after_nested_stub_class_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}
/// Returns `true` if the [`module_docstring_newlines`](https://github.com/astral-sh/ruff/issues/7995) preview style is enabled.
pub(crate) const fn is_module_docstring_newlines_enabled(context: &PyFormatContext) -> bool {
context.is_preview()

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