diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py index 296b4a79add90..73b8fe1b12cbc 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py @@ -1,3 +1,5 @@ +# comment + class Test( Aaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbbb, diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 06bc334262fa9..a492ef1012373 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -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], @@ -555,3 +554,69 @@ impl Format> 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> for FormatEmptyLinesAfterLeadingComments<'_> { + fn fmt(&self, f: &mut Formatter) -> 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(()) + } +} 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 4dcf95edcb489..8c1d1c3944033 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -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}; @@ -32,6 +34,29 @@ impl FormatNodeRule 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, [ 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 e55c5e3aa6e2c..5ad5f2f53904e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -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}; @@ -30,6 +32,29 @@ impl FormatNodeRule 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, [ diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap index e864b93275afd..43d3d15edad27 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap @@ -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. @@ -172,6 +172,7 @@ def f(): + +## + ++ +@decorator +def f(): + ... @@ -209,29 +210,50 @@ 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(): + ... + @@ -239,7 +261,7 @@ def f(): +## + + -+@dotted.decorator ++@dotted.decorator(kwarg=0) +def f(): + ... + @@ -247,7 +269,7 @@ def f(): +## + + -+@dotted.decorator(arg) ++@dotted.decorator(*args) +def f(): + ... + @@ -255,53 +277,32 @@ 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(): @@ -387,6 +388,7 @@ def f(): ## + @decorator def f(): ... diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap deleted file mode 100644 index 0bfa03ff16902..0000000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap +++ /dev/null @@ -1,86 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py ---- -## Input - -```py -#!/usr/bin/env python3.9 - -@relaxed_decorator[0] -def f(): - ... - -@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] -def f(): - ... - -@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") -def f(): - ... -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,6 +1,5 @@ - #!/usr/bin/env python3.9 - -- - @relaxed_decorator[0] - def f(): - ... -``` - -## Ruff Output - -```py -#!/usr/bin/env python3.9 - -@relaxed_decorator[0] -def f(): - ... - - -@relaxed_decorator[ - extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length -] -def f(): - ... - - -@extremely_long_variable_name_that_doesnt_fit := complex.expression( - with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" -) -def f(): - ... -``` - -## Black Output - -```py -#!/usr/bin/env python3.9 - - -@relaxed_decorator[0] -def f(): - ... - - -@relaxed_decorator[ - extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length -] -def f(): - ... - - -@extremely_long_variable_name_that_doesnt_fit := complex.expression( - with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" -) -def f(): - ... -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap index 0c009fa42416d..ba62839bd8149 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__form_feed.py.snap @@ -20,6 +20,7 @@ def test(): # fmt: on + def test(): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap index 9258c1ca71d98..752a76c22eb31 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -4,6 +4,8 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ --- ## Input ```py +# comment + class Test( Aaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbbb, @@ -232,6 +234,9 @@ class QuerySet(AltersData): ## Output ```py +# comment + + class Test( Aaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbbb,