Skip to content

Commit

Permalink
Implement blank_line_after_nested_stub_class preview style
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvmanila committed Jan 23, 2024
1 parent 9c8a4d9 commit 78028dc
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 12 deletions.
31 changes: 28 additions & 3 deletions crates/ruff_python_formatter/src/comments/format.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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_class_def;

/// Formats the leading comments of a node.
pub(crate) fn leading_node_comments<T>(node: &T) -> FormatLeadingComments
Expand Down Expand Up @@ -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_class_def(node, f)
{
1
} else {
0
}
}
(_, NodeLevel::TopLevel(_)) => 2,
(_, _) => 1,
};
Expand Down
7 changes: 7 additions & 0 deletions crates/ruff_python_formatter/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 5 additions & 2 deletions crates/ruff_python_formatter/src/statement/stmt_class_def.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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), AnyNodeRef::from(item))
.fmt(f)?;

Ok(())
}

fn fmt_dangling_comments(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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), AnyNodeRef::from(item))
.fmt(f)
}

fn fmt_dangling_comments(
Expand Down
88 changes: 83 additions & 5 deletions crates/ruff_python_formatter/src/statement/suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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::{
Expand Down Expand Up @@ -449,10 +451,67 @@ impl FormatRule<Suite, PyFormatContext<'_>> 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_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()
{
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_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:
Expand Down Expand Up @@ -480,7 +539,9 @@ 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
&& ((is_blank_line_after_nested_stub_class_enabled(f.context())
&& preceding.is_class_def_stmt())
|| lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1)
{
empty_line().fmt(f)
} else {
Expand All @@ -492,6 +553,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_class_def(AnyNodeRef::from(preceding), f);

// Two subsequent class definitions that both have an ellipsis only body
// ```python
// class A: ...
Expand Down Expand Up @@ -533,7 +610,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
}
Expand Down

0 comments on commit 78028dc

Please sign in to comment.