mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-30 16:33:49 +00:00
Correctly handle newlines after/before comments (#4895)
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary This issue fixes the removal of empty lines between a leading comment and the previous statement: ```python a = 20 # leading comment b = 10 ``` Ruff removed the empty line between `a` and `b` because: * The leading comments formatting does not preserve leading newlines (to avoid adding new lines at the top of a body) * The `JoinNodesBuilder` counted the lines before `b`, which is 1 -> Doesn't insert a new line This is fixed by changing the `JoinNodesBuilder` to count the lines instead *after* the last node. This correctly gives 1, and the `# leading comment` will insert the empty lines between any other leading comment or the node. ## Test Plan I added a new test for empty lines.
This commit is contained in:
parent
222ca98a41
commit
6ab3fc60f4
19 changed files with 332 additions and 124 deletions
|
@ -1,7 +1,10 @@
|
|||
use crate::context::NodeLevel;
|
||||
use crate::prelude::*;
|
||||
use ruff_formatter::{format_args, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
|
||||
use rustpython_parser::ast::{Stmt, Suite};
|
||||
use crate::trivia::lines_before;
|
||||
use ruff_formatter::{
|
||||
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
|
||||
};
|
||||
use rustpython_parser::ast::{Ranged, Stmt, Suite};
|
||||
|
||||
/// Level at which the [`Suite`] appears in the source code.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -13,6 +16,12 @@ pub enum SuiteLevel {
|
|||
Nested,
|
||||
}
|
||||
|
||||
impl SuiteLevel {
|
||||
const fn is_nested(self) -> bool {
|
||||
matches!(self, SuiteLevel::Nested)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormatSuite {
|
||||
level: SuiteLevel,
|
||||
|
@ -33,6 +42,9 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
SuiteLevel::Nested => NodeLevel::CompoundStatement,
|
||||
};
|
||||
|
||||
let comments = f.context().comments().clone();
|
||||
let source = f.context().contents();
|
||||
|
||||
let saved_level = f.context().node_level();
|
||||
f.context_mut().set_node_level(node_level);
|
||||
|
||||
|
@ -46,6 +58,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
// First entry has never any separator, doesn't matter which one we take;
|
||||
joiner.entry(first, &first.format());
|
||||
|
||||
let mut last = first;
|
||||
let mut is_last_function_or_class_definition = is_class_or_function_definition(first);
|
||||
|
||||
for statement in iter {
|
||||
|
@ -58,18 +71,59 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
|||
joiner.entry_with_separator(
|
||||
&format_args![empty_line(), empty_line()],
|
||||
&statement.format(),
|
||||
statement,
|
||||
);
|
||||
}
|
||||
SuiteLevel::Nested => {
|
||||
joiner
|
||||
.entry_with_separator(&format_args![empty_line()], &statement.format());
|
||||
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| {
|
||||
let start = if let Some(first_leading) =
|
||||
comments.leading_comments(statement.into()).first()
|
||||
{
|
||||
first_leading.slice().start()
|
||||
} else {
|
||||
statement.start()
|
||||
};
|
||||
|
||||
match lines_before(start, source) {
|
||||
0 | 1 => hard_line_break().fmt(f),
|
||||
2 => empty_line().fmt(f),
|
||||
3.. => {
|
||||
if self.level.is_nested() {
|
||||
empty_line().fmt(f)
|
||||
} else {
|
||||
write!(f, [empty_line(), empty_line()])
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
joiner.entry_with_separator(&separator, &statement.format(), statement);
|
||||
} else {
|
||||
joiner.entry(statement, &statement.format());
|
||||
}
|
||||
|
||||
is_last_function_or_class_definition = is_current_function_or_class_definition;
|
||||
last = statement;
|
||||
}
|
||||
|
||||
let result = joiner.finish();
|
||||
|
@ -87,6 +141,24 @@ const fn is_class_or_function_definition(stmt: &Stmt) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
const fn is_compound_statement(stmt: &Stmt) -> bool {
|
||||
matches!(
|
||||
stmt,
|
||||
Stmt::FunctionDef(_)
|
||||
| Stmt::AsyncFunctionDef(_)
|
||||
| Stmt::ClassDef(_)
|
||||
| Stmt::While(_)
|
||||
| Stmt::For(_)
|
||||
| Stmt::AsyncFor(_)
|
||||
| Stmt::Match(_)
|
||||
| Stmt::With(_)
|
||||
| Stmt::AsyncWith(_)
|
||||
| Stmt::If(_)
|
||||
| Stmt::Try(_)
|
||||
| Stmt::TryStar(_)
|
||||
)
|
||||
}
|
||||
|
||||
impl FormatRuleWithOptions<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
type Options = SuiteLevel;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue