Skip to content

Commit f25a245

Browse files
committed
Correctly handle newlines after/before comments
1 parent ec609f5 commit f25a245

19 files changed

+331
-123
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
# Removes the line above
3+
4+
a = 10 # Keeps the line above
5+
6+
# Separated by one line from `a` and `b`
7+
8+
b = 20
9+
# Adds two lines after `b`
10+
class Test:
11+
def a(self):
12+
pass
13+
# trailing comment
14+
15+
# two lines before, one line after
16+
17+
c = 30
18+
19+
while a == 10:
20+
...
21+
22+
# trailing comment with one line before
23+
24+
# one line before this leading comment
25+
26+
d = 40
27+
28+
while b == 20:
29+
...
30+
# no empty line before
31+
32+
e = 50 # one empty line before

crates/ruff_python_formatter/src/builders.rs

+46-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::context::NodeLevel;
22
use crate::prelude::*;
3-
use crate::trivia::lines_before;
3+
use crate::trivia::{lines_after, skip_trailing_trivia};
44
use ruff_formatter::write;
5+
use ruff_text_size::TextSize;
56
use rustpython_parser::ast::Ranged;
67

78
/// Provides Python specific extensions to [`Formatter`].
@@ -26,7 +27,7 @@ impl<'buf, 'ast> PyFormatterExtensions<'ast, 'buf> for PyFormatter<'ast, 'buf> {
2627
pub(crate) struct JoinNodesBuilder<'fmt, 'ast, 'buf> {
2728
fmt: &'fmt mut PyFormatter<'ast, 'buf>,
2829
result: FormatResult<()>,
29-
has_elements: bool,
30+
last_end: Option<TextSize>,
3031
node_level: NodeLevel,
3132
}
3233

@@ -35,7 +36,7 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> {
3536
Self {
3637
fmt,
3738
result: Ok(()),
38-
has_elements: false,
39+
last_end: None,
3940
node_level: level,
4041
}
4142
}
@@ -47,22 +48,43 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> {
4748
T: Ranged,
4849
{
4950
let node_level = self.node_level;
50-
let separator = format_with(|f: &mut PyFormatter| match node_level {
51-
NodeLevel::TopLevel => match lines_before(node.start(), f.context().contents()) {
52-
0 | 1 => hard_line_break().fmt(f),
53-
2 => empty_line().fmt(f),
54-
_ => write!(f, [empty_line(), empty_line()]),
55-
},
56-
NodeLevel::CompoundStatement => {
57-
match lines_before(node.start(), f.context().contents()) {
58-
0 | 1 => hard_line_break().fmt(f),
59-
_ => empty_line().fmt(f),
60-
}
51+
52+
self.result = self.result.and_then(|_| {
53+
if let Some(last_end) = self.last_end.replace(node.end()) {
54+
let source = self.fmt.context().contents();
55+
let count_lines = |offset| {
56+
// It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments
57+
// in the node's range
58+
// ```python
59+
// a # The range of `a` ends right before this comment
60+
//
61+
// b
62+
// ```
63+
//
64+
// Simply using `lines_after` doesn't work if a statement has a trailing comment because
65+
// it then counts the lines between the statement and the trailing comment, which is
66+
// always 0. This is why it skips any trailing trivia (trivia that's on the same line)
67+
// and counts the lines after.
68+
let after_trailing_trivia = skip_trailing_trivia(offset, source);
69+
lines_after(after_trailing_trivia, source)
70+
};
71+
72+
match node_level {
73+
NodeLevel::TopLevel => match dbg!(count_lines(last_end)) {
74+
0 | 1 => hard_line_break().fmt(self.fmt),
75+
2 => empty_line().fmt(self.fmt),
76+
_ => write!(self.fmt, [empty_line(), empty_line()]),
77+
},
78+
NodeLevel::CompoundStatement => match count_lines(last_end) {
79+
0 | 1 => hard_line_break().fmt(self.fmt),
80+
_ => empty_line().fmt(self.fmt),
81+
},
82+
NodeLevel::Expression => hard_line_break().fmt(self.fmt),
83+
}?;
6184
}
62-
NodeLevel::Expression => hard_line_break().fmt(f),
63-
});
6485

65-
self.entry_with_separator(&separator, content);
86+
content.fmt(self.fmt)
87+
});
6688
}
6789

6890
/// Writes a sequence of node with their content tuples, inserting the appropriate number of line breaks between any two of them
@@ -98,17 +120,20 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> {
98120
}
99121

100122
/// Writes a single entry using the specified separator to separate the entry from a previous entry.
101-
pub(crate) fn entry_with_separator(
123+
pub(crate) fn entry_with_separator<T>(
102124
&mut self,
103125
separator: &dyn Format<PyFormatContext<'ast>>,
104126
content: &dyn Format<PyFormatContext<'ast>>,
105-
) {
127+
node: &T,
128+
) where
129+
T: Ranged,
130+
{
106131
self.result = self.result.and_then(|_| {
107-
if self.has_elements {
132+
if self.last_end.is_some() {
108133
separator.fmt(self.fmt)?;
109134
}
110135

111-
self.has_elements = true;
136+
self.last_end = Some(node.end());
112137

113138
content.fmt(self.fmt)
114139
});

crates/ruff_python_formatter/src/lib.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,12 @@ Formatted twice:
355355
#[ignore]
356356
#[test]
357357
fn quick_test() {
358-
let src = r#"
359-
while True:
360-
if something.changed:
361-
do.stuff() # trailing comment
362-
other
358+
let src = r#"AAAAAAAAAAAAA = AAAAAAAAAAAAA # type: ignore
359+
360+
call_to_some_function_asdf(
361+
foo,
362+
[AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore
363+
)
363364
"#;
364365
// Tokenize once
365366
let mut tokens = Vec::new();

crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap

+8-21
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,7 @@ if True:
8484
```diff
8585
--- Black
8686
+++ Ruff
87-
@@ -1,75 +1,49 @@
88-
import core, time, a
89-
90-
from . import A, B, C
91-
-
92-
# keeps existing trailing comma
93-
from foo import (
94-
bar,
95-
)
96-
-
97-
# also keeps existing structure
98-
from foo import (
99-
baz,
100-
qux,
101-
)
102-
-
103-
# `as` works as well
104-
from foo import (
87+
@@ -18,44 +18,26 @@
10588
xyzzy as magic,
10689
)
10790
@@ -154,12 +137,12 @@ if True:
154137
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s"
155138
- % bar
156139
-)
157-
-
158140
+y = {"oneple": (1,),}
159141
+assert False, ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" % bar)
142+
160143
# looping over a 1-tuple should also not get wrapped
161144
for x in (1,):
162-
pass
145+
@@ -63,13 +45,9 @@
163146
for (x,) in (1,), (2,), (3,):
164147
pass
165148
@@ -175,7 +158,7 @@ if True:
175158
print("foo %r", (foo.bar,))
176159
177160
if True:
178-
@@ -79,21 +53,15 @@
161+
@@ -79,21 +57,15 @@
179162
)
180163
181164
if True:
@@ -210,15 +193,18 @@ if True:
210193
import core, time, a
211194
212195
from . import A, B, C
196+
213197
# keeps existing trailing comma
214198
from foo import (
215199
bar,
216200
)
201+
217202
# also keeps existing structure
218203
from foo import (
219204
baz,
220205
qux,
221206
)
207+
222208
# `as` works as well
223209
from foo import (
224210
xyzzy as magic,
@@ -244,6 +230,7 @@ nested_long_lines = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb
244230
x = {"oneple": (1,)}
245231
y = {"oneple": (1,),}
246232
assert False, ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" % bar)
233+
247234
# looping over a 1-tuple should also not get wrapped
248235
for x in (1,):
249236
pass

crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap

+1-5
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,8 @@ x = [
3030
```diff
3131
--- Black
3232
+++ Ruff
33-
@@ -10,6 +10,9 @@
34-
1, 2,
35-
3, 4,
33+
@@ -12,4 +12,6 @@
3634
]
37-
+
3835
# fmt: on
3936
4037
-x = [1, 2, 3, 4]
@@ -58,7 +55,6 @@ x = [
5855
1, 2,
5956
3, 4,
6057
]
61-
6258
# fmt: on
6359
6460
x = [

crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap

+6-13
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,7 @@ elif unformatted:
9797
```diff
9898
--- Black
9999
+++ Ruff
100-
@@ -9,8 +9,6 @@
101-
] # Includes an formatted indentation.
102-
},
103-
)
104-
-
105-
-
106-
# Regression test for https://github.com/psf/black/issues/2015.
107-
run(
108-
# fmt: off
109-
@@ -44,7 +42,7 @@
100+
@@ -44,7 +44,7 @@
110101
print ( "This won't be formatted" )
111102
print ( "This won't be formatted either" )
112103
else:
@@ -115,7 +106,7 @@ elif unformatted:
115106
116107
117108
# Regression test for https://github.com/psf/black/issues/3184.
118-
@@ -61,7 +59,7 @@
109+
@@ -61,7 +61,7 @@
119110
elif param[0:4] in ("ZZZZ",):
120111
print ( "This won't be formatted either" )
121112
@@ -124,7 +115,7 @@ elif unformatted:
124115
125116
126117
# Regression test for https://github.com/psf/black/issues/2985.
127-
@@ -72,10 +70,7 @@
118+
@@ -72,10 +72,7 @@
128119
129120
130121
class Factory(t.Protocol):
@@ -136,7 +127,7 @@ elif unformatted:
136127
137128
138129
# Regression test for https://github.com/psf/black/issues/3436.
139-
@@ -83,5 +78,5 @@
130+
@@ -83,5 +80,5 @@
140131
return x
141132
# fmt: off
142133
elif unformatted:
@@ -160,6 +151,8 @@ setup(
160151
] # Includes an formatted indentation.
161152
},
162153
)
154+
155+
163156
# Regression test for https://github.com/psf/black/issues/2015.
164157
run(
165158
# fmt: off

crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap

+3-5
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,8 @@ some_module.some_function(
188188
):
189189
pass
190190
191-
@@ -100,15 +56,7 @@
192-
some_module.some_function(
193-
argument1, (one_element_tuple,), argument4, argument5, argument6
194-
)
195-
-
191+
@@ -103,12 +59,5 @@
192+
196193
# Inner trailing comma causes outer to explode
197194
some_module.some_function(
198195
- argument1,
@@ -268,6 +265,7 @@ def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_cr
268265
some_module.some_function(
269266
argument1, (one_element_tuple,), argument4, argument5, argument6
270267
)
268+
271269
# Inner trailing comma causes outer to explode
272270
some_module.some_function(
273271
argument1, (one, two,), argument4, argument5, argument6

crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap

+3-6
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ __all__ = (
6262
```diff
6363
--- Black
6464
+++ Ruff
65-
@@ -2,12 +2,13 @@
65+
@@ -2,8 +2,10 @@
6666
6767
# flake8: noqa
6868
@@ -74,11 +74,7 @@ __all__ = (
7474
ERROR,
7575
)
7676
import sys
77-
-
78-
# This relies on each of the submodules having an __all__ variable.
79-
from .base_events import *
80-
from .coroutines import *
81-
@@ -22,33 +23,16 @@
77+
@@ -22,33 +24,16 @@
8278
from ..streams import *
8379
8480
from some_library import (
@@ -134,6 +130,7 @@ from logging import (
134130
ERROR,
135131
)
136132
import sys
133+
137134
# This relies on each of the submodules having an __all__ variable.
138135
from .base_events import *
139136
from .coroutines import *

0 commit comments

Comments
 (0)