Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Insert necessary blank line between class and leading comments #8224

Merged
merged 1 commit into from Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,3 +1,5 @@
# comment

class Test(
Aaaaaaaaaaaaaaaaa,
Bbbbbbbbbbbbbbbb,
Expand Down
69 changes: 67 additions & 2 deletions crates/ruff_python_formatter/src/comments/format.rs
Expand Up @@ -507,13 +507,12 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
///
/// For example, given:
/// ```python
/// def func():
/// class Class:
/// ...
/// # comment
/// ```
///
/// This builder will insert two empty lines before the comment.
/// ```
pub(crate) fn empty_lines_before_trailing_comments<'a>(
f: &PyFormatter,
comments: &'a [SourceComment],
Expand Down Expand Up @@ -555,3 +554,69 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
Ok(())
}
}

/// Format the empty lines between a node and its leading comments.
///
/// For example, given:
/// ```python
/// # comment
///
/// class Class:
/// ...
/// ```
///
/// While `leading_comments` will preserve the existing empty line, this builder will insert an
/// additional empty line before the comment.
pub(crate) fn empty_lines_after_leading_comments<'a>(
f: &PyFormatter,
comments: &'a [SourceComment],
) -> FormatEmptyLinesAfterLeadingComments<'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,
(_, NodeLevel::TopLevel) => 2,
(_, _) => 1,
};

FormatEmptyLinesAfterLeadingComments {
comments,
empty_lines,
}
}

#[derive(Copy, Clone, Debug)]
pub(crate) struct FormatEmptyLinesAfterLeadingComments<'a> {
/// The leading comments of the node.
comments: &'a [SourceComment],
/// The expected number of empty lines after the leading comments.
empty_lines: u32,
}

impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
if let Some(comment) = self
.comments
.iter()
.rev()
.find(|comment| comment.line_position().is_own_line())
{
let actual = lines_after(comment.end(), f.context().source()).saturating_sub(1);
// If there are no empty lines, keep the comment tight to the node.
if actual == 0 {
return Ok(());
}

// If there are more than enough empty lines already, `leading_comments` will
// trim them as necessary.
if actual >= self.empty_lines {
return Ok(());
}

for _ in actual..self.empty_lines {
write!(f, [empty_line()])?;
}
}
Ok(())
}
}
27 changes: 26 additions & 1 deletion crates/ruff_python_formatter/src/statement/stmt_class_def.rs
Expand Up @@ -3,7 +3,9 @@ use ruff_python_ast::{Decorator, StmtClassDef};
use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia;
use ruff_text_size::Ranged;

use crate::comments::format::empty_lines_before_trailing_comments;
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
};
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::prelude::*;
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
Expand Down Expand Up @@ -32,6 +34,29 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);

// If the class contains leading comments, insert newlines before them.
// For example, given:
// ```python
// # comment
//
// class Class:
// ...
// ```
//
// At the top-level in a non-stub file, reformat as:
// ```python
// # comment
//
//
// class Class:
// ...
// ```
// Note that this is only really relevant for the specific case in which there's a single
// newline between the comment and the node, but we _require_ two newlines. If there are
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
// are more than two, then `leading_comments` will preserve the correct number of newlines.
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;

write!(
f,
[
Expand Down
@@ -1,7 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::StmtFunctionDef;

use crate::comments::format::empty_lines_before_trailing_comments;
use crate::comments::format::{
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize};
Expand Down Expand Up @@ -30,6 +32,29 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);

// If the class contains leading comments, insert newlines before them.
// For example, given:
// ```python
// # comment
//
// def func():
// ...
// ```
//
// At the top-level in a non-stub file, reformat as:
// ```python
// # comment
//
//
// def func():
// ...
// ```
// Note that this is only really relevant for the specific case in which there's a single
// newline between the comment and the node, but we _require_ two newlines. If there are
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
// are more than two, then `leading_comments` will preserve the correct number of newlines.
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;

write!(
f,
[
Expand Down
Expand Up @@ -162,7 +162,7 @@ def f():
```diff
--- Black
+++ Ruff
@@ -1,29 +1,205 @@
@@ -1,29 +1,206 @@
+# This file doesn't use the standard decomposition.
+# Decorator syntax test cases are separated by double # comments.
+# Those before the 'output' comment are valid under the old syntax.
Expand All @@ -172,6 +172,7 @@ def f():
+
+##
+
+
+@decorator
+def f():
+ ...
Expand Down Expand Up @@ -209,99 +210,99 @@ def f():
+ ...
+
+
+##
+
##

-@decorator()()
+
+@decorator(**kwargs)
+def f():
+ ...
+
+
+##
def f():
...

+
##

-@(decorator)
+
+@decorator(*args, **kwargs)
+def f():
+ ...
+
+
+##
def f():
...

+
##

-@sequence["decorator"]
+
+@decorator(
+ *args,
+ **kwargs,
+)
def f():
...

+
##

-@decorator[List[str]]
+
+@dotted.decorator
def f():
...

+
##

-@var := decorator
+
+@dotted.decorator(arg)
+def f():
+ ...
+
+
+##
+
+
+@dotted.decorator
+@dotted.decorator(kwarg=0)
+def f():
+ ...
+
+
+##
+
+
+@dotted.decorator(arg)
+@dotted.decorator(*args)
+def f():
+ ...
+
+
+##
+
+
+@dotted.decorator(kwarg=0)
+@dotted.decorator(**kwargs)
+def f():
+ ...
+
+
##

-@decorator()()
+##
+
+@dotted.decorator(*args)
def f():
...

+
##
-@(decorator)
+@dotted.decorator(*args, **kwargs)
+def f():
+ ...
+
+@dotted.decorator(**kwargs)
def f():
...

+
##

-@sequence["decorator"]
+
+@dotted.decorator(*args, **kwargs)
def f():
...

+##
+
##

-@decorator[List[str]]
+
+@dotted.decorator(
+ *args,
+ **kwargs,
+)
def f():
...

+def f():
+ ...
+
+
+##
+
##

-@var := decorator
+
+@double.dotted.decorator
+def f():
Expand Down Expand Up @@ -387,6 +388,7 @@ def f():

##


@decorator
def f():
...
Expand Down