diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py index 17edab372513da..f419cb9b047eb7 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py @@ -97,6 +97,45 @@ class ByteDocstring: b""" has leading whitespace""" first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 3b6bde34f666c2..c9298d4a901faf 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -2,7 +2,10 @@ use std::cmp::Ordering; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::{self as ast, Comprehension, Expr, MatchCase, ModModule, Parameters}; +use ruff_python_ast::{ + self as ast, AnyNodeRef, Comprehension, Expr, ExprStringLiteral, MatchCase, ModModule, + Parameters, +}; use ruff_python_trivia::{ find_only_token_in_range, indentation_at_offset, BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, SimpleTokenizer, @@ -544,6 +547,37 @@ fn handle_own_line_comment_between_statements<'a>( return CommentPlacement::Default(comment); } + if comment.line_position().is_end_of_line() { + return CommentPlacement::Default(comment); + } + + // Comments after docstrings need a newline between the docstring and the comment, so we attach + // it as leading on the first statement after the docstring. + // (https://github.com/astral-sh/ruff/issues/7948) + // ```python + // class ModuleBrowser: + // """Browse module classes and functions in IDLE.""" + // # ^ Insert a newline above here + // + // def __init__(self, master, path, *, _htest=False, _utest=False): + // pass + // ``` + if let AnyNodeRef::StmtExpr(preceding_expr) = preceding { + // We can't use `is_docstring_stmt` here because there is no `AnyNodeRef` to `&Stmt` + // conversion + if is_first_statement_in_body(preceding, comment.enclosing_node()) + && matches!( + preceding_expr.value.as_ref(), + Expr::StringLiteral(ExprStringLiteral { + implicit_concatenated: false, + .. + }) + ) + { + return CommentPlacement::leading(following, comment); + } + } + // If the comment is directly attached to the following statement; make it a leading // comment: // ```python diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index a32184f5b7da3e..16dc8252e71ea9 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -584,9 +584,31 @@ impl Format> for DocstringStmt<'_> { string_literal .format() .with_options(StringLayout::DocString), - trailing_comments(node_comments.trailing), ] - ) + )?; + + // Comments after docstrings need a newline between the docstring and the comment, so we attach + // it as leading on the first statement after the docstring. + // (https://github.com/astral-sh/ruff/issues/7948) + // ```python + // class ModuleBrowser: + // """Browse module classes and functions in IDLE.""" + // # ^ Insert a newline above here + // + // def __init__(self, master, path, *, _htest=False, _utest=False): + // pass + // ``` + if let Some(own_line) = node_comments + .trailing + .iter() + .find(|comment| comment.line_position().is_own_line()) + { + if lines_before(own_line.start(), f.context().source()) < 2 { + empty_line().fmt(f)?; + } + } + + trailing_comments(node_comments.trailing).fmt(f) } } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index ce417c728b56be..ef41670ec02318 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -225,8 +225,11 @@ d={'a':1, # fmt: on goes + here, andhere, -@@ -122,8 +123,10 @@ +@@ -120,10 +121,13 @@ + + The comments between will be formatted. This is a known limitation. """ ++ # fmt: off - # hey, that won't work @@ -237,7 +240,7 @@ d={'a':1, # fmt: on pass -@@ -138,7 +141,7 @@ +@@ -138,7 +142,7 @@ now . considers . multiple . fmt . directives . within . one . prefix # fmt: on # fmt: off @@ -246,7 +249,7 @@ d={'a':1, # fmt: on -@@ -178,14 +181,18 @@ +@@ -178,14 +182,18 @@ $ """, # fmt: off @@ -395,6 +398,7 @@ def off_and_on_without_data(): The comments between will be formatted. This is a known limitation. """ + # fmt: off diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap index 580b55562af07a..134dee817aa372 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -103,6 +103,45 @@ class ByteDocstring: b""" has leading whitespace""" first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting @@ -222,6 +261,46 @@ class ByteDocstring: first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting @@ -341,6 +420,46 @@ class ByteDocstring: first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting @@ -460,6 +579,46 @@ class ByteDocstring: first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting @@ -579,6 +738,46 @@ class ByteDocstring: first_statement = 1 +class CommentAfterDocstring1: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring2: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + def __init__(self): + pass + + +class CommentAfterDocstring3: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring4: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + def __init__(self): + pass + + +class CommentAfterDocstring5: + """Browse module classes and functions in IDLE.""" + + # This class is also the base class for pathbrowser.PathBrowser. + + class TabbedIndent: def tabbed_indent(self): """check for correct tabbed formatting