From facb6f8bda8ab10b251510715a9a6d30d365db60 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 24 Jan 2024 12:09:36 +0530 Subject: [PATCH] Implement `blank_line_after_nested_stub_class` preview style --- .../src/comments/format.rs | 31 ++++- crates/ruff_python_formatter/src/preview.rs | 7 ++ .../src/statement/stmt_class_def.rs | 7 +- .../src/statement/stmt_function_def.rs | 5 +- .../src/statement/suite.rs | 111 ++++++++++++++++-- 5 files changed, 146 insertions(+), 15 deletions(-) diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index abf4833dac59d..ff1dec1feca8b 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -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, 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::is_last_child_a_class_def; /// Formats the leading comments of a node. pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments @@ -513,14 +514,38 @@ 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: AnyNodeRef<'_>, ) -> 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, _) => { + if is_blank_line_after_nested_stub_class_enabled(f.context()) + && node.is_stmt_class_def() + && !is_last_child_a_class_def(node, f) + { + 1 + } else { + 0 + } + } (_, NodeLevel::TopLevel(_)) => 2, (_, _) => 1, }; diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index f610da5fd41d3..0f7d8d1d323c8 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -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() diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 8c1d1c3944033..b93ebc7457a36 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,5 +1,5 @@ use ruff_formatter::write; -use ruff_python_ast::{Decorator, StmtClassDef}; +use ruff_python_ast::{AnyNodeRef, Decorator, StmtClassDef}; use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia; use ruff_text_size::Ranged; @@ -152,7 +152,10 @@ impl FormatNodeRule for FormatStmtClassDef { // // # comment // ``` - empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f) + empty_lines_before_trailing_comments(f, comments.trailing(item), AnyNodeRef::from(item)) + .fmt(f)?; + + Ok(()) } fn fmt_dangling_comments( diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 5ad5f2f53904e..2be18eb97d9e6 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -1,5 +1,5 @@ use ruff_formatter::write; -use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::{AnyNodeRef, StmtFunctionDef}; use crate::comments::format::{ empty_lines_after_leading_comments, empty_lines_before_trailing_comments, @@ -87,7 +87,8 @@ impl FormatNodeRule for FormatStmtFunctionDef { // // # comment // ``` - empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f) + empty_lines_before_trailing_comments(f, comments.trailing(item), AnyNodeRef::from(item)) + .fmt(f) } fn fmt_dangling_comments( diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 7623ef9449e90..627e9115c863a 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -2,7 +2,9 @@ use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWi use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite}; -use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before}; +use ruff_python_trivia::{ + lines_after, lines_after_ignoring_end_of_line_trivia, lines_before, SimpleTokenizer, +}; use ruff_text_size::{Ranged, TextRange}; use crate::comments::{ @@ -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::{ @@ -449,10 +451,72 @@ impl FormatRule> for FormatSuite { empty_line_after_docstring = false; } + // For preview style in stub files, add an empty line after the last class + // definition if the body isn't empty. + // + // If it has any trailing comments, then the same is handled by the + // `empty_lines_before_trailing_comments` builder, so we ignore those: + // + // ```python + // class Top: + // class Nested: + // pass + // # comment + // ``` + // + // Now, if the class is at the end of file we need to be careful here. + // If there's nothing after the class, we don't want to add an empty line. + // + // ```python + // if something: + // class Nested: + // pass + // ``` + // + // But, if there's a top-level comment after the class, we need to add + // an empty line: + // + // ```python + // if something: + // class Nested: + // pass + // # comment + // ``` + // + // Here, an empty line should be added before the comment. + if is_blank_line_after_nested_stub_class_enabled(f.context()) + && source_type.is_stub() + && self.kind != SuiteKind::TopLevel + && !comments.has_trailing(preceding) + && preceding + .as_class_def_stmt() + .is_some_and(|class| !is_last_child_a_class_def(AnyNodeRef::from(class), f)) + && SimpleTokenizer::starts_at(preceding.end(), source) + .skip_while(|token| token.kind.is_trivia() && !token.kind.is_comment()) + .next() + .is_some() + { + match lines_after(preceding.end(), source) { + 0 => hard_line_break().fmt(f)?, + _ => { + empty_line().fmt(f)?; + } + } + } + Ok(()) } } +/// Checks if the last child of the given node is a class definition without a +/// trailing comment. +pub(crate) fn is_last_child_a_class_def(node: AnyNodeRef<'_>, f: &PyFormatter) -> bool { + let comments = f.context().comments(); + std::iter::successors(node.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()) +} + /// Stub files have bespoke rules for empty lines. /// /// These rules are ported from black (preview mode at time of writing) using the stubs test case: @@ -479,10 +543,24 @@ fn stub_file_empty_lines( } } SuiteKind::Class | SuiteKind::Other | SuiteKind::Function => { - if empty_line_condition - && lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1 - { - empty_line().fmt(f) + if empty_line_condition { + if is_blank_line_after_nested_stub_class_enabled(f.context()) { + if preceding.as_class_def_stmt().is_some_and(|class| { + if contains_only_an_ellipsis(&class.body, f.context().comments()) { + matches!(following, Stmt::ClassDef(_) | Stmt::FunctionDef(_)) + } else { + !is_last_child_a_class_def(AnyNodeRef::from(preceding), f) + } + }) { + empty_line().fmt(f) + } else { + hard_line_break().fmt(f) + } + } else if lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1 { + empty_line().fmt(f) + } else { + hard_line_break().fmt(f) + } } else { hard_line_break().fmt(f) } @@ -492,6 +570,22 @@ fn stub_file_empty_lines( /// Only a function to compute it lazily fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyFormatter) -> bool { + // Preceding node contains a class definition as its last statement in the body. + // ```python + // class A: + // class B: + // pass + // class C: + // pass + // ``` + // + // Here, the preceding node is `class A` and the following node is `class C`. + // The empty line between `class A` and `class C` should be omitted if preview + // style is enabled because it'll be added by `class B` formatting. + let preceding_has_class_as_last_stmt = + is_no_blank_line_before_class_docstring_enabled(f.context()) + && is_last_child_a_class_def(AnyNodeRef::from(preceding), f); + // Two subsequent class definitions that both have an ellipsis only body // ```python // class A: ... @@ -533,7 +627,8 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm .is_some_and(|function| contains_only_an_ellipsis(&function.body, f.context().comments())) && following.is_function_def_stmt(); - class_sequences_with_ellipsis_only + preceding_has_class_as_last_stmt + || class_sequences_with_ellipsis_only || class_decorator_instead_of_empty_line || function_with_ellipsis }