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:
Micha Reiser 2023-06-07 14:49:43 +02:00 committed by GitHub
parent 222ca98a41
commit 6ab3fc60f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 332 additions and 124 deletions

View file

@ -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;