mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:10 +00:00
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:
parent
79f0522eb7
commit
541aef4e6c
13 changed files with 891 additions and 29 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue