mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-23 21:15:19 +00:00
Remove newline-insertion logic from JoinNodesBuilder
(#6205)
## Summary This PR moves the "insert empty lines" behavior out of `JoinNodesBuilder` and into the `Suite` formatter. I find it a little confusing that the logic is split between those two formatters right now, and since this is _only_ used in that one place, IMO it is a bit simpler to just inline it and use a single approach to tracking state (right now, both are stateful). The only other place this was used was for decorators. As a side effect, we now remove blank lines in both of these cases, which is a known but intentional deviation from Black (which preserves the empty line before the comment in the first case): ```python @foo # Hello @bar def baz(): pass @foo @bar def baz(): pass ```
This commit is contained in:
parent
6ee5cb37c0
commit
615337a54d
4 changed files with 111 additions and 303 deletions
|
@ -1,10 +1,7 @@
|
|||
use ruff_python_ast::{Ranged, Stmt, Suite};
|
||||
|
||||
use ruff_formatter::{
|
||||
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
|
||||
};
|
||||
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_trivia::lines_before;
|
||||
use ruff_python_ast::{Ranged, Stmt, Suite};
|
||||
use ruff_python_trivia::{lines_after, lines_before, skip_trailing_trivia};
|
||||
|
||||
use crate::context::NodeLevel;
|
||||
use crate::prelude::*;
|
||||
|
@ -51,52 +48,50 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
let saved_level = f.context().node_level();
|
||||
f.context_mut().set_node_level(node_level);
|
||||
|
||||
let mut joiner = f.join_nodes(node_level);
|
||||
// Wrap the entire formatting operation in a `format_with` to ensure that we restore
|
||||
// context regardless of whether an error occurs.
|
||||
let formatted = format_with(|f| {
|
||||
let mut iter = statements.iter();
|
||||
let Some(first) = iter.next() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut iter = statements.iter();
|
||||
let Some(first) = iter.next() else {
|
||||
return Ok(());
|
||||
};
|
||||
// First entry has never any separator, doesn't matter which one we take.
|
||||
write!(f, [first.format()])?;
|
||||
|
||||
// First entry has never any separator, doesn't matter which one we take.
|
||||
joiner.entry(first, &first.format());
|
||||
let mut last = first;
|
||||
|
||||
let mut last = first;
|
||||
|
||||
for statement in iter {
|
||||
if is_class_or_function_definition(last) || is_class_or_function_definition(statement) {
|
||||
match self.level {
|
||||
SuiteLevel::TopLevel => {
|
||||
joiner.entry_with_separator(
|
||||
&format_args![empty_line(), empty_line()],
|
||||
&statement.format(),
|
||||
statement,
|
||||
);
|
||||
for statement in iter {
|
||||
if is_class_or_function_definition(last)
|
||||
|| is_class_or_function_definition(statement)
|
||||
{
|
||||
match self.level {
|
||||
SuiteLevel::TopLevel => {
|
||||
write!(f, [empty_line(), empty_line(), statement.format()])?;
|
||||
}
|
||||
SuiteLevel::Nested => {
|
||||
write!(f, [empty_line(), statement.format()])?;
|
||||
}
|
||||
}
|
||||
SuiteLevel::Nested => {
|
||||
joiner.entry_with_separator(&empty_line(), &statement.format(), statement);
|
||||
}
|
||||
}
|
||||
} else if is_import_definition(last) && !is_import_definition(statement) {
|
||||
joiner.entry_with_separator(&empty_line(), &statement.format(), statement);
|
||||
} else if is_compound_statement(last) {
|
||||
// Handles the case where a body has trailing comments. The issue is that RustPython does not include
|
||||
// the comments in the range of the suite. This means, the body ends right after the last statement in the body.
|
||||
// ```python
|
||||
// def test():
|
||||
// ...
|
||||
// # The body of `test` ends right after `...` and before this comment
|
||||
//
|
||||
// # leading comment
|
||||
//
|
||||
//
|
||||
// a = 10
|
||||
// ```
|
||||
// Using `lines_after` for the node doesn't work because it would count the lines after the `...`
|
||||
// which is 0 instead of 1, the number of lines between the trailing comment and
|
||||
// the leading comment. This is why the suite handling counts the lines before the
|
||||
// start of the next statement or before the first leading comments for compound statements.
|
||||
let separator = format_with(|f| {
|
||||
} else if is_import_definition(last) && !is_import_definition(statement) {
|
||||
write!(f, [empty_line(), statement.format()])?;
|
||||
} else if is_compound_statement(last) {
|
||||
// Handles the case where a body has trailing comments. The issue is that RustPython does not include
|
||||
// the comments in the range of the suite. This means, the body ends right after the last statement in the body.
|
||||
// ```python
|
||||
// def test():
|
||||
// ...
|
||||
// # The body of `test` ends right after `...` and before this comment
|
||||
//
|
||||
// # leading comment
|
||||
//
|
||||
//
|
||||
// a = 10
|
||||
// ```
|
||||
// Using `lines_after` for the node doesn't work because it would count the lines after the `...`
|
||||
// which is 0 instead of 1, the number of lines between the trailing comment and
|
||||
// the leading comment. This is why the suite handling counts the lines before the
|
||||
// start of the next statement or before the first leading comments for compound statements.
|
||||
let start =
|
||||
if let Some(first_leading) = comments.leading_comments(statement).first() {
|
||||
first_leading.slice().start()
|
||||
|
@ -105,27 +100,66 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
};
|
||||
|
||||
match lines_before(start, source) {
|
||||
0 | 1 => hard_line_break().fmt(f),
|
||||
2 => empty_line().fmt(f),
|
||||
0 | 1 => write!(f, [hard_line_break()])?,
|
||||
2 => write!(f, [empty_line()])?,
|
||||
3.. => {
|
||||
if self.level.is_nested() {
|
||||
empty_line().fmt(f)
|
||||
write!(f, [empty_line()])?;
|
||||
} else {
|
||||
write!(f, [empty_line(), empty_line()])
|
||||
write!(f, [empty_line(), empty_line()])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
joiner.entry_with_separator(&separator, &statement.format(), statement);
|
||||
} else {
|
||||
joiner.entry(statement, &statement.format());
|
||||
write!(f, [statement.format()])?;
|
||||
} else {
|
||||
// Insert the appropriate number of empty lines based on the node level, e.g.:
|
||||
// * [`NodeLevel::Module`]: Up to two empty lines
|
||||
// * [`NodeLevel::CompoundStatement`]: Up to one empty line
|
||||
// * [`NodeLevel::Expression`]: No empty lines
|
||||
|
||||
let count_lines = |offset| {
|
||||
// It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments
|
||||
// in the node's range
|
||||
// ```python
|
||||
// a # The range of `a` ends right before this comment
|
||||
//
|
||||
// b
|
||||
// ```
|
||||
//
|
||||
// Simply using `lines_after` doesn't work if a statement has a trailing comment because
|
||||
// it then counts the lines between the statement and the trailing comment, which is
|
||||
// always 0. This is why it skips any trailing trivia (trivia that's on the same line)
|
||||
// and counts the lines after.
|
||||
let after_trailing_trivia = skip_trailing_trivia(offset, source);
|
||||
lines_after(after_trailing_trivia, source)
|
||||
};
|
||||
|
||||
match node_level {
|
||||
NodeLevel::TopLevel => match count_lines(last.end()) {
|
||||
0 | 1 => write!(f, [hard_line_break()])?,
|
||||
2 => write!(f, [empty_line()])?,
|
||||
_ => write!(f, [empty_line(), empty_line()])?,
|
||||
},
|
||||
NodeLevel::CompoundStatement => match count_lines(last.end()) {
|
||||
0 | 1 => write!(f, [hard_line_break()])?,
|
||||
_ => write!(f, [empty_line()])?,
|
||||
},
|
||||
NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => {
|
||||
write!(f, [hard_line_break()])?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(f, [statement.format()])?;
|
||||
}
|
||||
|
||||
last = statement;
|
||||
}
|
||||
|
||||
last = statement;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let result = joiner.finish();
|
||||
let result = formatted.fmt(f);
|
||||
|
||||
f.context_mut().set_node_level(saved_level);
|
||||
|
||||
|
@ -133,6 +167,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a [`Stmt`] is a class or function definition.
|
||||
const fn is_class_or_function_definition(stmt: &Stmt) -> bool {
|
||||
matches!(
|
||||
stmt,
|
||||
|
@ -140,6 +175,7 @@ const fn is_class_or_function_definition(stmt: &Stmt) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if a [`Stmt`] is an import.
|
||||
const fn is_import_definition(stmt: &Stmt) -> bool {
|
||||
matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_))
|
||||
}
|
||||
|
@ -171,11 +207,10 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_formatter::format;
|
||||
use ruff_python_ast::Suite;
|
||||
use ruff_python_parser::Parse;
|
||||
|
||||
use ruff_formatter::format;
|
||||
|
||||
use crate::comments::Comments;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::suite::SuiteLevel;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue