From 1743ef8398228554a07a7e167c7f5be111edbf9c Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 13 Oct 2023 16:04:01 +0200 Subject: [PATCH] Remove empty line before raw dostrings **Summary** This fixes a deviation with black where black would remove empty lines before raw docstrings for some reason. --- .../ruff/statement/class_definition.py | 5 ++++ .../src/expression/string.rs | 10 +++---- .../src/statement/suite.rs | 27 +++++++++---------- ...format@statement__class_definition.py.snap | 8 ++++++ 4 files changed, 30 insertions(+), 20 deletions(-) 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 296b4a79ad..1ce05fbd8f 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 @@ -102,6 +102,11 @@ class Test: x = 1 +class EmptyLineBeforeRawDocstring: + + r"""Character and line based layer over a BufferedIOBase object, buffer.""" + + class C(): # comment pass diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 7e0b0cb06b..fa07f7727e 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -496,7 +496,7 @@ impl Format> for NormalizedString<'_> { bitflags! { #[derive(Copy, Clone, Debug, PartialEq, Eq)] - pub(super) struct StringPrefix: u8 { + pub(crate) struct StringPrefix: u8 { const UNICODE = 0b0000_0001; /// `r"test"` const RAW = 0b0000_0010; @@ -508,7 +508,7 @@ bitflags! { } impl StringPrefix { - pub(super) fn parse(input: &str) -> StringPrefix { + pub(crate) fn parse(input: &str) -> StringPrefix { let chars = input.chars(); let mut prefix = StringPrefix::empty(); @@ -533,15 +533,15 @@ impl StringPrefix { prefix } - pub(super) const fn text_len(self) -> TextSize { + pub(crate) const fn text_len(self) -> TextSize { TextSize::new(self.bits().count_ones()) } - pub(super) const fn is_raw_string(self) -> bool { + pub(crate) const fn is_raw_string(self) -> bool { self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER) } - pub(super) const fn is_fstring(self) -> bool { + pub(crate) const fn is_fstring(self) -> bool { self.contains(StringPrefix::F_STRING) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 9575f78d86..490799e980 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -10,7 +10,7 @@ use crate::comments::{ }; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::expr_constant::ExprConstantLayout; -use crate::expression::string::StringLayout; +use crate::expression::string::{StringLayout, StringPrefix}; use crate::prelude::*; use crate::statement::stmt_expr::FormatStmtExpr; use crate::verbatim::{ @@ -99,9 +99,13 @@ impl FormatRule> for FormatSuite { SuiteKind::Class => { if let Some(docstring) = DocstringStmt::try_from_statement(first) { + let prefix = + StringPrefix::parse(f.context().locator().slice(docstring.0.range())); if !comments.has_leading(first) && lines_before(first.start(), source) > 1 && !source_type.is_stub() + // For some reason black removes the empty line before raw docstrings + && !prefix.is_raw_string() { // Allow up to one empty line before a class docstring, e.g., this is // stable formatting: @@ -484,8 +488,10 @@ impl<'ast> IntoFormat> for Suite { } /// A statement representing a docstring. +/// +/// We keep both the outer statement and the inner constant here for convenience. #[derive(Copy, Clone)] -pub(crate) struct DocstringStmt<'a>(&'a Stmt); +pub(crate) struct DocstringStmt<'a>(&'a Stmt, &'a ExprConstant); impl<'a> DocstringStmt<'a> { /// Checks if the statement is a simple string that can be formatted as a docstring @@ -494,9 +500,9 @@ impl<'a> DocstringStmt<'a> { return None; }; - if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() { - if !value.is_implicit_concatenated() { - return Some(DocstringStmt(stmt)); + if let Expr::Constant(expr_constant @ ExprConstant { value, .. }) = value.as_ref() { + if (value.is_str() || value.is_unicode_string()) && !value.is_implicit_concatenated() { + return Some(DocstringStmt(stmt, expr_constant)); } } @@ -512,21 +518,12 @@ impl Format> for DocstringStmt<'_> { if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) { suppressed_node(self.0).fmt(f) } else { - // SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ConstantExpr`. - let constant = self - .0 - .as_expr_stmt() - .unwrap() - .value - .as_constant_expr() - .unwrap(); - // We format the expression, but the statement carries the comments write!( f, [ leading_comments(node_comments.leading), - constant + self.1 .format() .with_options(ExprConstantLayout::String(StringLayout::DocString)), trailing_comments(node_comments.trailing), 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 9258c1ca71..a49d0cd04f 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 @@ -108,6 +108,10 @@ class Test: x = 1 +class EmptyLineBeforeRawDocstring: + + r"""Character and line based layer over a BufferedIOBase object, buffer.""" + class C(): # comment pass @@ -356,6 +360,10 @@ class Test: x = 1 +class EmptyLineBeforeRawDocstring: + r"""Character and line based layer over a BufferedIOBase object, buffer.""" + + class C: # comment pass