Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,96 @@ def overload4():
# trailing comment

def overload4(a: int): ...


# In preview, we preserve these newlines at the start of functions:
def preserved1():

return 1

def preserved2():

pass

def preserved3():

def inner(): ...

def preserved4():

def inner():
print("with a body")
return 1

return 2

def preserved5():

...
# trailing comment prevents collapsing the stub


def preserved6():

# Comment

return 1


def preserved7():

# comment
# another line
# and a third

return 0


def preserved8(): # this also prevents collapsing the stub

...


# But we still discard these newlines:
def removed1():

"Docstring"

return 1


def removed2():

...


def removed3():

... # trailing same-line comment does not prevent collapsing the stub


# And we discard empty lines after the first:
def partially_preserved1():


return 1
Comment on lines +406 to +410
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add more tests to this, specifically tests including comments or cases where the first element is a function on its own?



# We only preserve blank lines, not add new ones
def untouched1():
# comment

return 0


def untouched2():
# comment
return 0


def untouched3():
# comment
# another line
# and a third

return 0
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,9 @@ def test6 ():
print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )


def test7 ():
<RANGE_START>print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )
7 changes: 7 additions & 0 deletions crates/ruff_python_formatter/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ pub(crate) const fn is_remove_parens_around_except_types_enabled(
) -> bool {
context.is_preview()
}

/// Returns `true` if the
/// [`allow_newline_after_block_open`](https://github.com/astral-sh/ruff/pull/21110) preview style
/// is enabled.
pub(crate) const fn is_allow_newline_after_block_open_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
13 changes: 3 additions & 10 deletions crates/ruff_python_formatter/src/statement/clause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};

use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, contains_only_an_ellipsis};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::verbatim::write_suppressed_clause_header;
use crate::{has_skip_comment, prelude::*};

Expand Down Expand Up @@ -449,17 +449,10 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
|| matches!(self.kind, SuiteKind::Function | SuiteKind::Class);

if should_collapse_stub
&& contains_only_an_ellipsis(self.body, f.context().comments())
&& let Some(ellipsis) = as_only_an_ellipsis(self.body, f.context().comments())
&& self.trailing_comments.is_empty()
{
write!(
f,
[
space(),
self.body.format().with_options(self.kind),
hard_line_break()
]
)
write!(f, [space(), ellipsis.format(), hard_line_break()])
} else {
write!(
f,
Expand Down
46 changes: 34 additions & 12 deletions crates/ruff_python_formatter/src/statement/suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use crate::comments::{
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
use crate::other::string_literal::StringLiteralKind;
use crate::prelude::*;
use crate::preview::is_blank_line_before_decorated_class_in_stub_enabled;
use crate::preview::{
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
};
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
Expand Down Expand Up @@ -169,6 +171,22 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
false,
)
} else {
// Allow an empty line after a function header in preview, if the function has no
// docstring and no initial comment.
let allow_newline_after_block_open =
is_allow_newline_after_block_open_enabled(f.context())
&& matches!(self.kind, SuiteKind::Function)
&& matches!(first, SuiteChildStatement::Other(_));

let start = comments
.leading(first)
.first()
.map_or_else(|| first.start(), Ranged::start);

if allow_newline_after_block_open && lines_before(start, f.context().source()) > 1 {
empty_line().fmt(f)?;
}

first.fmt(f)?;

let empty_line_after_docstring = if matches!(first, SuiteChildStatement::Docstring(_))
Expand Down Expand Up @@ -218,7 +236,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
)?;
} else {
// Preserve empty lines after a stub implementation but don't insert a new one if there isn't any present in the source.
// This is useful when having multiple function overloads that should be grouped to getter by omitting new lines between them.
// This is useful when having multiple function overloads that should be grouped together by omitting new lines between them.
let is_preceding_stub_function_without_empty_line = following
.is_function_def_stmt()
&& preceding
Expand Down Expand Up @@ -728,17 +746,21 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm

/// Returns `true` if a function or class body contains only an ellipsis with no comments.
pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool {
match body {
[Stmt::Expr(ast::StmtExpr { value, .. })] => {
let [node] = body else {
return false;
};
value.is_ellipsis_literal_expr()
&& !comments.has_leading(node)
&& !comments.has_trailing_own_line(node)
}
_ => false,
as_only_an_ellipsis(body, comments).is_some()
}

/// Returns `Some(Stmt::Ellipsis)` if a function or class body contains only an ellipsis with no
/// comments.
pub(crate) fn as_only_an_ellipsis<'a>(body: &'a [Stmt], comments: &Comments) -> Option<&'a Stmt> {
if let [node @ Stmt::Expr(ast::StmtExpr { value, .. })] = body
&& value.is_ellipsis_literal_expr()
&& !comments.has_leading(node)
&& !comments.has_trailing_own_line(node)
{
return Some(node);
}

None
}

/// Returns `true` if a [`Stmt`] is a class or function definition.
Expand Down
Loading