From 09296e3e3cf18eeea1b6f6391fb43e50e49af00a Mon Sep 17 00:00:00 2001
From: Dhruv Manilawala <dhruvmanila@gmail.com>
Date: Tue, 19 Dec 2023 00:43:20 -0600
Subject: [PATCH] Implement `no_blank_line_before_class_docstring` preview
 style (#9154)

## Summary

This PR implements the `no_blank_line_before_class_docstring` preview
style.

## Test Plan

Update existing snapshots.

### Formatter ecosystem

`main`

| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |

`dhruv/no-blank-line-docstring`

| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |

fixes: #8888
---
 ...k_line_before_class_docstring.options.json |   5 +
 .../ruff/blank_line_before_class_docstring.py |  38 +++++
 crates/ruff_python_formatter/src/preview.rs   |   9 ++
 .../src/statement/suite.rs                    |  11 ++
 ...iew_no_blank_line_before_docstring.py.snap | 134 ------------------
 ...compatibility@cases__raw_docstring.py.snap |  14 +-
 ...@blank_line_before_class_docstring.py.snap |  95 +++++++++++++
 .../tests/snapshots/format@preview.py.snap    |   1 -
 ...format@statement__class_definition.py.snap |  25 +++-
 9 files changed, 184 insertions(+), 148 deletions(-)
 create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.options.json
 create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
 delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_no_blank_line_before_docstring.py.snap
 create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap

diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.options.json
new file mode 100644
index 0000000000000..8925dd0a8280f
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.options.json
@@ -0,0 +1,5 @@
+[
+  {
+    "preview": "enabled"
+  }
+]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
new file mode 100644
index 0000000000000..a8dbbafebbfd1
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
@@ -0,0 +1,38 @@
+class NormalDocstring:
+
+    """This is a docstring."""
+
+
+class DocstringWithComment0:
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment1:
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment2:
+
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment3:
+
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment4:
+
+
+    # This is a comment
+
+
+    """This is a docstring."""
+
+
diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs
index 90379d1ad8fab..4de87cf05c911 100644
--- a/crates/ruff_python_formatter/src/preview.rs
+++ b/crates/ruff_python_formatter/src/preview.rs
@@ -24,3 +24,12 @@ pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled(
 ) -> bool {
     context.is_preview()
 }
+
+/// Returns `true` if the [`no_blank_line_before_class_docstring`] preview style is enabled.
+///
+/// [`no_blank_line_before_class_docstring`]: https://github.com/astral-sh/ruff/issues/8888
+pub(crate) const fn is_no_blank_line_before_class_docstring_enabled(
+    context: &PyFormatContext,
+) -> bool {
+    context.is_preview()
+}
diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs
index 8b3d9e1a5efbd..f811e882f2524 100644
--- a/crates/ruff_python_formatter/src/statement/suite.rs
+++ b/crates/ruff_python_formatter/src/statement/suite.rs
@@ -11,6 +11,7 @@ use crate::comments::{
 use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
 use crate::expression::expr_string_literal::ExprStringLiteralKind;
 use crate::prelude::*;
+use crate::preview::is_no_blank_line_before_class_docstring_enabled;
 use crate::statement::stmt_expr::FormatStmtExpr;
 use crate::verbatim::{
     suppressed_node, write_suppressed_statements_starting_with_leading_comment,
@@ -108,14 +109,24 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
                     if !comments.has_leading(first)
                         && lines_before(first.start(), source) > 1
                         && !source_type.is_stub()
+                        && !is_no_blank_line_before_class_docstring_enabled(f.context())
                     {
                         // Allow up to one empty line before a class docstring, e.g., this is
                         // stable formatting:
+                        //
                         // ```python
                         // class Test:
                         //
                         //     """Docstring"""
                         // ```
+                        //
+                        // But, in preview mode, we don't want to allow any empty lines before a
+                        // class docstring, e.g., this is preview formatting:
+                        //
+                        // ```python
+                        // class Test:
+                        //   """Docstring"""
+                        // ```
                         empty_line().fmt(f)?;
                     }
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_no_blank_line_before_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_no_blank_line_before_docstring.py.snap
deleted file mode 100644
index bd93e24292838..0000000000000
--- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_no_blank_line_before_docstring.py.snap
+++ /dev/null
@@ -1,134 +0,0 @@
----
-source: crates/ruff_python_formatter/tests/fixtures.rs
-input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_no_blank_line_before_docstring.py
----
-## Input
-
-```python
-def line_before_docstring():
-
-    """Please move me up"""
-
-
-class LineBeforeDocstring:
-
-    """Please move me up"""
-
-
-class EvenIfThereIsAMethodAfter:
-
-    """I'm the docstring"""
-    def method(self):
-        pass
-
-
-class TwoLinesBeforeDocstring:
-
-
-    """I want to be treated the same as if I were closer"""
-
-
-class MultilineDocstringsAsWell:
-
-    """I'm so far
-
-    and on so many lines...
-    """
-```
-
-## Black Differences
-
-```diff
---- Black
-+++ Ruff
-@@ -3,10 +3,12 @@
- 
- 
- class LineBeforeDocstring:
-+
-     """Please move me up"""
- 
- 
- class EvenIfThereIsAMethodAfter:
-+
-     """I'm the docstring"""
- 
-     def method(self):
-@@ -14,10 +16,12 @@
- 
- 
- class TwoLinesBeforeDocstring:
-+
-     """I want to be treated the same as if I were closer"""
- 
- 
- class MultilineDocstringsAsWell:
-+
-     """I'm so far
- 
-     and on so many lines...
-```
-
-## Ruff Output
-
-```python
-def line_before_docstring():
-    """Please move me up"""
-
-
-class LineBeforeDocstring:
-
-    """Please move me up"""
-
-
-class EvenIfThereIsAMethodAfter:
-
-    """I'm the docstring"""
-
-    def method(self):
-        pass
-
-
-class TwoLinesBeforeDocstring:
-
-    """I want to be treated the same as if I were closer"""
-
-
-class MultilineDocstringsAsWell:
-
-    """I'm so far
-
-    and on so many lines...
-    """
-```
-
-## Black Output
-
-```python
-def line_before_docstring():
-    """Please move me up"""
-
-
-class LineBeforeDocstring:
-    """Please move me up"""
-
-
-class EvenIfThereIsAMethodAfter:
-    """I'm the docstring"""
-
-    def method(self):
-        pass
-
-
-class TwoLinesBeforeDocstring:
-    """I want to be treated the same as if I were closer"""
-
-
-class MultilineDocstringsAsWell:
-    """I'm so far
-
-    and on so many lines...
-    """
-```
-
-
diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__raw_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__raw_docstring.py.snap
index 8b27585265471..bc66189376a79 100644
--- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__raw_docstring.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__raw_docstring.py.snap
@@ -27,30 +27,21 @@ class UpperCaseR:
 ```diff
 --- Black
 +++ Ruff
-@@ -1,4 +1,5 @@
- class C:
-+
-     r"""Raw"""
- 
- 
-@@ -7,8 +8,9 @@
+@@ -7,7 +7,7 @@
  
  
  class SingleQuotes:
 -    r'''Raw'''
- 
 +    r"""Raw"""
-+
+ 
  
  class UpperCaseR:
-     R"""Raw"""
 ```
 
 ## Ruff Output
 
 ```python
 class C:
-
     r"""Raw"""
 
 
@@ -59,7 +50,6 @@ def f():
 
 
 class SingleQuotes:
-
     r"""Raw"""
 
 
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap
new file mode 100644
index 0000000000000..b3986d0395ff1
--- /dev/null
+++ b/crates/ruff_python_formatter/tests/snapshots/format@blank_line_before_class_docstring.py.snap
@@ -0,0 +1,95 @@
+---
+source: crates/ruff_python_formatter/tests/fixtures.rs
+input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/blank_line_before_class_docstring.py
+---
+## Input
+```python
+class NormalDocstring:
+
+    """This is a docstring."""
+
+
+class DocstringWithComment0:
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment1:
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment2:
+
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment3:
+
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment4:
+
+
+    # This is a comment
+
+
+    """This is a docstring."""
+
+
+```
+
+## Outputs
+### Output 1
+```
+indent-style               = space
+line-width                 = 88
+indent-width               = 4
+quote-style                = Double
+line-ending                = LineFeed
+magic-trailing-comma       = Respect
+docstring-code             = Disabled
+docstring-code-line-width  = "dynamic"
+preview                    = Enabled
+```
+
+```python
+class NormalDocstring:
+    """This is a docstring."""
+
+
+class DocstringWithComment0:
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment1:
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment2:
+    # This is a comment
+    """This is a docstring."""
+
+
+class DocstringWithComment3:
+    # This is a comment
+
+    """This is a docstring."""
+
+
+class DocstringWithComment4:
+    # This is a comment
+
+    """This is a docstring."""
+```
+
+
+
diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
index fdedcce512dd8..888c6f938a304 100644
--- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
+++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap
@@ -198,7 +198,6 @@ def reference_docstring_newlines():
 
 
 class RemoveNewlineBeforeClassDocstring:
-
     """Black's `Preview.no_blank_line_before_class_docstring`"""
 
 
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 de82b7126c37a..eec7f540f77b5 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
@@ -513,7 +513,30 @@ class QuerySet(AltersData):
  
  
  class Test(
-@@ -159,20 +158,17 @@
+@@ -94,7 +93,6 @@
+ 
+ 
+ class Test:
+-
+     """Docstring"""
+ 
+ 
+@@ -111,14 +109,12 @@
+ 
+ 
+ class Test:
+-
+     """Docstring"""
+ 
+     x = 1
+ 
+ 
+ class Test:
+-
+     """Docstring"""
+ 
+     # comment
+@@ -159,20 +155,17 @@
  
  @dataclass
  # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP