diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.options.json new file mode 100644 index 0000000000000..e8f35d9827789 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.options.json @@ -0,0 +1,6 @@ +[ + { + "source_type": "Stub", + "preview": "enabled" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.pyi b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.pyi new file mode 100644 index 0000000000000..3ce823d9ae765 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.pyi @@ -0,0 +1,183 @@ +class Top1: + pass +class Top2: + pass + +class Top: + class Ellipsis: ... + class Ellipsis: ... + +class Top: + class Ellipsis: ... + class Pass: + pass + +class Top: + class Ellipsis: ... + class_variable = 1 + +class Top: + class TrailingComment: + pass + # comment + class Other: + pass + +class Top: + class CommentWithEllipsis: ... + # comment + class Other: ... + +class Top: + class TrailingCommentWithMultipleBlankLines: + pass + + + # comment + class Other: + pass + +class Top: + class Nested: + pass + + # comment + class LeadingComment: + pass + +class Top: + @decorator + class Ellipsis: ... + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + @decorator + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + @decorator + class Pass: + pass + +class Top: + class Foo: + pass + + + + + class AfterMultipleEmptyLines: + pass + +class Top: + class Nested11: + class Nested12: + pass + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + # comment + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + # comment + class Nested21: + pass + # comment + +class Top1: + class Nested: + pass +class Top2: + pass + +class Top1: + class Nested: + pass + # comment +class Top2: + pass + +class Top1: + class Nested: + pass +# comment +class Top2: + pass + +if foo: + class Nested1: + pass + class Nested2: + pass +else: + pass + +if foo: + class Nested1: + pass + class Nested2: + pass + # comment +elif bar: + class Nested1: + pass +# comment +else: + pass + +if top1: + class Nested: + pass +if top2: + pass + +if top1: + class Nested: + pass + # comment +if top2: + pass + +if top1: + class Nested: + pass +# comment +if top2: + pass + +try: + class Try: + pass +except: + class Except: + pass +foo = 1 + +match foo: + case 1: + class Nested: + pass + case 2: + class Nested: + pass + case _: + class Nested: + pass +foo = 1 + +class Eof: + class Nested: + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.options.json new file mode 100644 index 0000000000000..e8f35d9827789 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.options.json @@ -0,0 +1,6 @@ +[ + { + "source_type": "Stub", + "preview": "enabled" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.pyi b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.pyi new file mode 100644 index 0000000000000..50007377e19e1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.pyi @@ -0,0 +1,17 @@ +# A separate file to test out the behavior when there are a mix of blank lines +# and comments at EOF just after a nested stub class. + +class Top: + class Nested1: + class Nested12: + pass + # comment + class Nested2: + pass + + + +# comment + + + diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index abf4833dac59d..e1690ef5c261d 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,8 +1,7 @@ use std::borrow::Cow; use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode}; -use ruff_python_ast::PySourceType; -use ruff_python_ast::{AnyNodeRef, AstNode}; +use ruff_python_ast::{AnyNodeRef, AstNode, NodeKind, PySourceType}; use ruff_python_trivia::{ is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before, }; @@ -11,6 +10,8 @@ use ruff_text_size::{Ranged, TextLen, TextRange}; use crate::comments::{CommentLinePosition, SourceComment}; use crate::context::NodeLevel; use crate::prelude::*; +use crate::preview::is_blank_line_after_nested_stub_class_enabled; +use crate::statement::suite::should_insert_blank_line_after_class_in_stub_file; /// Formats the leading comments of a node. pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments @@ -85,7 +86,11 @@ pub(crate) struct FormatLeadingAlternateBranchComments<'a> { impl Format> for FormatLeadingAlternateBranchComments<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - if let Some(first_leading) = self.comments.first() { + if self.last_node.map_or(false, |preceding| { + should_insert_blank_line_after_class_in_stub_file(preceding, None, f.context()) + }) { + write!(f, [empty_line(), leading_comments(self.comments)])?; + } else if let Some(first_leading) = self.comments.first() { // Leading comments only preserves the lines after the comment but not before. // Insert the necessary lines. write!( @@ -513,14 +518,32 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> { /// ``` /// /// This builder will insert two empty lines before the comment. +/// +/// # Preview +/// +/// For preview style, this builder will insert a single empty line after a +/// class definition in a stub file. +/// +/// For example, given: +/// ```python +/// class Foo: +/// pass +/// # comment +/// ``` +/// +/// This builder will insert a single empty line before the comment. pub(crate) fn empty_lines_before_trailing_comments<'a>( f: &PyFormatter, comments: &'a [SourceComment], + node_kind: NodeKind, ) -> FormatEmptyLinesBeforeTrailingComments<'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, + (PySourceType::Stub, _) => u32::from( + is_blank_line_after_nested_stub_class_enabled(f.context()) + && node_kind == NodeKind::StmtClassDef, + ), (_, NodeLevel::TopLevel(_)) => 2, (_, _) => 1, }; diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index f610da5fd41d3..0f7d8d1d323c8 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -48,6 +48,13 @@ pub(crate) const fn is_wrap_multiple_context_managers_in_parens_enabled( context.is_preview() } +/// Returns `true` if the [`blank_line_after_nested_stub_class`](https://github.com/astral-sh/ruff/issues/8891) preview style is enabled. +pub(crate) const fn is_blank_line_after_nested_stub_class_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} + /// Returns `true` if the [`module_docstring_newlines`](https://github.com/astral-sh/ruff/issues/7995) preview style is enabled. pub(crate) const fn is_module_docstring_newlines_enabled(context: &PyFormatContext) -> bool { context.is_preview() 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 8c1d1c3944033..a53004a0694b9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,5 +1,5 @@ use ruff_formatter::write; -use ruff_python_ast::{Decorator, StmtClassDef}; +use ruff_python_ast::{Decorator, NodeKind, StmtClassDef}; use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia; use ruff_text_size::Ranged; @@ -152,7 +152,10 @@ impl FormatNodeRule for FormatStmtClassDef { // // # comment // ``` - empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f) + empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtClassDef) + .fmt(f)?; + + Ok(()) } fn fmt_dangling_comments( 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 5ad5f2f53904e..17957f5df1dd8 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -1,5 +1,5 @@ use ruff_formatter::write; -use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::{NodeKind, StmtFunctionDef}; use crate::comments::format::{ empty_lines_after_leading_comments, empty_lines_before_trailing_comments, @@ -87,7 +87,8 @@ impl FormatNodeRule for FormatStmtFunctionDef { // // # comment // ``` - empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f) + empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtFunctionDef) + .fmt(f) } fn fmt_dangling_comments( diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 7623ef9449e90..efcf6eff3d75b 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -1,4 +1,6 @@ -use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_formatter::{ + write, FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, +}; use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite}; @@ -12,8 +14,8 @@ use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, With use crate::expression::expr_string_literal::ExprStringLiteralKind; use crate::prelude::*; use crate::preview::{ - is_dummy_implementations_enabled, is_module_docstring_newlines_enabled, - is_no_blank_line_before_class_docstring_enabled, + is_blank_line_after_nested_stub_class_enabled, is_dummy_implementations_enabled, + is_module_docstring_newlines_enabled, is_no_blank_line_before_class_docstring_enabled, }; use crate::statement::stmt_expr::FormatStmtExpr; use crate::verbatim::{ @@ -470,17 +472,23 @@ fn stub_file_empty_lines( let empty_line_condition = preceding_comments.has_trailing() || following_comments.has_leading() || !stub_suite_can_omit_empty_line(preceding, following, f); + let require_empty_line = should_insert_blank_line_after_class_in_stub_file( + preceding.into(), + Some(following.into()), + f.context(), + ); match kind { SuiteKind::TopLevel => { - if empty_line_condition { + if empty_line_condition || require_empty_line { empty_line().fmt(f) } else { hard_line_break().fmt(f) } } SuiteKind::Class | SuiteKind::Other | SuiteKind::Function => { - if empty_line_condition - && lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1 + if (empty_line_condition + && lines_after_ignoring_end_of_line_trivia(preceding.end(), source) > 1) + || require_empty_line { empty_line().fmt(f) } else { @@ -490,6 +498,122 @@ fn stub_file_empty_lines( } } +/// Checks if an empty line should be inserted after a class definition. +/// +/// This is only valid if the [`blank_line_after_nested_stub_class`](https://github.com/astral-sh/ruff/issues/8891) +/// preview rule is enabled and the source to be formatted is a stub file. +/// +/// If `following` is `None`, then the preceding node is the last one in a suite. The +/// caller needs to make sure that the suite which the preceding node is part of is +/// followed by an alternate branch and shouldn't be a top-level suite. +pub(crate) fn should_insert_blank_line_after_class_in_stub_file( + preceding: AnyNodeRef<'_>, + following: Option>, + context: &PyFormatContext, +) -> bool { + if !(is_blank_line_after_nested_stub_class_enabled(context) + && context.options().source_type().is_stub()) + { + return false; + } + let comments = context.comments(); + match preceding.as_stmt_class_def() { + Some(class) if contains_only_an_ellipsis(&class.body, comments) => { + let Some(following) = following else { + // The formatter is at the start of an alternate branch such as + // an `else` block. + // + // ```python + // if foo: + // class Nested: + // pass + // else: + // pass + // ``` + // + // In the above code, the preceding node is the `Nested` class + // which has no following node. + return true; + }; + + // If the preceding class has decorators, then we need to add an empty + // line even if it only contains ellipsis. + // + // ```python + // class Top: + // @decorator + // class Nested1: ... + // foo = 1 + // ``` + let preceding_has_decorators = !class.decorator_list.is_empty(); + + // If the following statement is a class definition, then an empty line + // should be inserted if it (1) doesn't just contain ellipsis, or (2) has decorators. + // + // ```python + // class Top: + // class Nested1: ... + // class Nested2: + // pass + // + // class Top: + // class Nested1: ... + // @decorator + // class Nested2: ... + // ``` + // + // Both of the above examples should add a blank line in between. + let following_is_class_without_only_ellipsis_or_has_decorators = + following.as_stmt_class_def().is_some_and(|following| { + !contains_only_an_ellipsis(&following.body, comments) + || !following.decorator_list.is_empty() + }); + + preceding_has_decorators + || following_is_class_without_only_ellipsis_or_has_decorators + || following.is_stmt_function_def() + } + Some(_) => { + // Preceding statement is a class definition whose body isn't only an ellipsis. + // Here, we should only add a blank line if the class doesn't have a trailing + // own line comment as that's handled by the class formatting itself. + !comments.has_trailing_own_line(preceding) + } + None => { + // If preceding isn't a class definition, let's check if the last statement + // in the body, going all the way down, is a class definition. + // + // ```python + // if foo: + // if bar: + // class Nested: + // pass + // if other: + // pass + // ``` + // + // But, if it contained a trailing own line comment, then it's handled + // by the class formatting itself. + // + // ```python + // if foo: + // if bar: + // class Nested: + // pass + // # comment + // if other: + // pass + // ``` + std::iter::successors( + preceding.last_child_in_body(), + AnyNodeRef::last_child_in_body, + ) + .take_while(|last_child| !comments.has_trailing_own_line(*last_child)) + .any(|last_child| last_child.is_stmt_class_def()) + } + } +} + /// Only a function to compute it lazily fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyFormatter) -> bool { // Two subsequent class definitions that both have an ellipsis only body diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__nested_stub.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__nested_stub.pyi.snap index 45761c1051684..5eed350fd7dbc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__nested_stub.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__nested_stub.pyi.snap @@ -42,17 +42,15 @@ class TopLevel: ```diff --- Black +++ Ruff -@@ -3,33 +3,27 @@ +@@ -3,7 +3,6 @@ class Outer: class InnerStub: ... outer_attr_after_inner_stub: int - class Inner: inner_attr: int -- - outer_attr: int - if sys.version_info > (3, 7): +@@ -13,12 +12,10 @@ if sys.platform == "win32": assignment = 1 def function_definition(self): ... @@ -65,17 +63,6 @@ class TopLevel: def f2(self) -> str: ... class TopLevel: - class Nested1: - foo: int - def bar(self): ... -- - field = 1 - - class Nested2: - def bar(self): ... - foo: int -- - field = 1 ``` ## Ruff Output @@ -88,6 +75,7 @@ class Outer: outer_attr_after_inner_stub: int class Inner: inner_attr: int + outer_attr: int if sys.version_info > (3, 7): @@ -104,11 +92,13 @@ class TopLevel: class Nested1: foo: int def bar(self): ... + field = 1 class Nested2: def bar(self): ... foo: int + field = 1 ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap new file mode 100644 index 0000000000000..4f570bba9ffa1 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class.pyi.snap @@ -0,0 +1,419 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class.pyi +--- +## Input +```python +class Top1: + pass +class Top2: + pass + +class Top: + class Ellipsis: ... + class Ellipsis: ... + +class Top: + class Ellipsis: ... + class Pass: + pass + +class Top: + class Ellipsis: ... + class_variable = 1 + +class Top: + class TrailingComment: + pass + # comment + class Other: + pass + +class Top: + class CommentWithEllipsis: ... + # comment + class Other: ... + +class Top: + class TrailingCommentWithMultipleBlankLines: + pass + + + # comment + class Other: + pass + +class Top: + class Nested: + pass + + # comment + class LeadingComment: + pass + +class Top: + @decorator + class Ellipsis: ... + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + @decorator + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + @decorator + class Pass: + pass + +class Top: + class Foo: + pass + + + + + class AfterMultipleEmptyLines: + pass + +class Top: + class Nested11: + class Nested12: + pass + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + # comment + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + # comment + class Nested21: + pass + # comment + +class Top1: + class Nested: + pass +class Top2: + pass + +class Top1: + class Nested: + pass + # comment +class Top2: + pass + +class Top1: + class Nested: + pass +# comment +class Top2: + pass + +if foo: + class Nested1: + pass + class Nested2: + pass +else: + pass + +if foo: + class Nested1: + pass + class Nested2: + pass + # comment +elif bar: + class Nested1: + pass +# comment +else: + pass + +if top1: + class Nested: + pass +if top2: + pass + +if top1: + class Nested: + pass + # comment +if top2: + pass + +if top1: + class Nested: + pass +# comment +if top2: + pass + +try: + class Try: + pass +except: + class Except: + pass +foo = 1 + +match foo: + case 1: + class Nested: + pass + case 2: + class Nested: + pass + case _: + class Nested: + pass +foo = 1 + +class Eof: + class Nested: + pass +``` + +## 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 +target_version = Py38 +source_type = Stub +``` + +```python +class Top1: + pass + +class Top2: + pass + +class Top: + class Ellipsis: ... + class Ellipsis: ... + +class Top: + class Ellipsis: ... + + class Pass: + pass + +class Top: + class Ellipsis: ... + class_variable = 1 + +class Top: + class TrailingComment: + pass + + # comment + class Other: + pass + +class Top: + class CommentWithEllipsis: ... + # comment + class Other: ... + +class Top: + class TrailingCommentWithMultipleBlankLines: + pass + + # comment + class Other: + pass + +class Top: + class Nested: + pass + + # comment + class LeadingComment: + pass + +class Top: + @decorator + class Ellipsis: ... + + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + + @decorator + class Ellipsis: ... + +class Top: + @decorator + class Ellipsis: ... + + @decorator + class Pass: + pass + +class Top: + class Foo: + pass + + class AfterMultipleEmptyLines: + pass + +class Top: + class Nested11: + class Nested12: + pass + + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + + # comment + + class Nested21: + pass + +class Top: + class Nested11: + class Nested12: + pass + + # comment + class Nested21: + pass + + # comment + +class Top1: + class Nested: + pass + +class Top2: + pass + +class Top1: + class Nested: + pass + + # comment + +class Top2: + pass + +class Top1: + class Nested: + pass + +# comment +class Top2: + pass + +if foo: + class Nested1: + pass + + class Nested2: + pass + +else: + pass + +if foo: + class Nested1: + pass + + class Nested2: + pass + + # comment +elif bar: + class Nested1: + pass + +# comment +else: + pass + +if top1: + class Nested: + pass + +if top2: + pass + +if top1: + class Nested: + pass + + # comment +if top2: + pass + +if top1: + class Nested: + pass + +# comment +if top2: + pass + +try: + class Try: + pass + +except: + class Except: + pass + +foo = 1 + +match foo: + case 1: + class Nested: + pass + + case 2: + class Nested: + pass + + case _: + class Nested: + pass + +foo = 1 + +class Eof: + class Nested: + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap new file mode 100644 index 0000000000000..18410103151b7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__blank_line_after_nested_stub_class_eof.pyi.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/blank_line_after_nested_stub_class_eof.pyi +--- +## Input +```python +# A separate file to test out the behavior when there are a mix of blank lines +# and comments at EOF just after a nested stub class. + +class Top: + class Nested1: + class Nested12: + pass + # comment + class Nested2: + pass + + + +# comment + + + +``` + +## 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 +target_version = Py38 +source_type = Stub +``` + +```python +# A separate file to test out the behavior when there are a mix of blank lines +# and comments at EOF just after a nested stub class. + +class Top: + class Nested1: + class Nested12: + pass + + # comment + + class Nested2: + pass + +# comment +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap index 9edc789b5fabd..cea732f045fe3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__suite.pyi.snap @@ -309,4 +309,27 @@ class ComplexStatements: ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -110,6 +110,7 @@ + + class InnerClass5: + def a(self): ... ++ + field1 = 1 + + class InnerClass6: +@@ -119,6 +120,7 @@ + + class InnerClass7: + def a(self): ... ++ + print("hi") + + class InnerClass8: +``` + +