Skip to content

Commit 9b6d2ce

Browse files
authored
Fix incorect placement of trailing stub function comments (#11632)
1 parent 889667a commit 9b6d2ce

File tree

6 files changed

+142
-44
lines changed

6 files changed

+142
-44
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Regression tests for https://github.com/astral-sh/ruff/issues/11569
2+
3+
4+
# comment 1
5+
def foo(self) -> None: ...
6+
def bar(self) -> None: ...
7+
# comment 2
8+
9+
# comment 3
10+
def baz(self) -> None:
11+
return None
12+
# comment 4
13+
14+
15+
def foo(self) -> None: ...
16+
# comment 5
17+
18+
def baz(self) -> None:
19+
return None
20+
21+
22+
def foo(self) -> None:
23+
... # comment 5
24+
def baz(self) -> None:
25+
return None
26+
27+
def foo(self) -> None: ...
28+
# comment 5

crates/ruff_python_formatter/src/comments/format.rs

+31-39
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ impl Format<PyFormatContext<'_>> for FormatTrailingComments<'_> {
164164
line_suffix(
165165
&format_args![
166166
empty_lines(lines_before_comment),
167-
format_comment(trailing)
167+
format_comment(trailing),
168168
],
169169
// Reserving width isn't necessary because we don't split
170170
// comments and the empty lines expand any enclosing group.
@@ -535,31 +535,21 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
535535
/// ```
536536
///
537537
/// This builder will insert a single empty line before the comment.
538-
pub(crate) fn empty_lines_before_trailing_comments<'a>(
539-
f: &PyFormatter,
540-
comments: &'a [SourceComment],
538+
pub(crate) fn empty_lines_before_trailing_comments(
539+
comments: &[SourceComment],
541540
node_kind: NodeKind,
542-
) -> FormatEmptyLinesBeforeTrailingComments<'a> {
543-
// Black has different rules for stub vs. non-stub and top level vs. indented
544-
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
545-
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
546-
(PySourceType::Stub, _) => u32::from(node_kind == NodeKind::StmtClassDef),
547-
(_, NodeLevel::TopLevel(_)) => 2,
548-
(_, _) => 1,
549-
};
550-
541+
) -> FormatEmptyLinesBeforeTrailingComments {
551542
FormatEmptyLinesBeforeTrailingComments {
552543
comments,
553-
empty_lines,
544+
node_kind,
554545
}
555546
}
556547

557548
#[derive(Copy, Clone, Debug)]
558549
pub(crate) struct FormatEmptyLinesBeforeTrailingComments<'a> {
559550
/// The trailing comments of the node.
560551
comments: &'a [SourceComment],
561-
/// The expected number of empty lines before the trailing comments.
562-
empty_lines: u32,
552+
node_kind: NodeKind,
563553
}
564554

565555
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
@@ -569,9 +559,17 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
569559
.iter()
570560
.find(|comment| comment.line_position().is_own_line())
571561
{
562+
// Black has different rules for stub vs. non-stub and top level vs. indented
563+
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
564+
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
565+
(PySourceType::Stub, _) => u32::from(self.node_kind == NodeKind::StmtClassDef),
566+
(_, NodeLevel::TopLevel(_)) => 2,
567+
(_, _) => 1,
568+
};
569+
572570
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
573-
for _ in actual..self.empty_lines {
574-
write!(f, [empty_line()])?;
571+
for _ in actual..empty_lines {
572+
empty_line().fmt(f)?;
575573
}
576574
}
577575
Ok(())
@@ -590,30 +588,16 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
590588
///
591589
/// While `leading_comments` will preserve the existing empty line, this builder will insert an
592590
/// additional empty line before the comment.
593-
pub(crate) fn empty_lines_after_leading_comments<'a>(
594-
f: &PyFormatter,
595-
comments: &'a [SourceComment],
596-
) -> FormatEmptyLinesAfterLeadingComments<'a> {
597-
// Black has different rules for stub vs. non-stub and top level vs. indented
598-
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
599-
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
600-
(PySourceType::Stub, _) => 0,
601-
(_, NodeLevel::TopLevel(_)) => 2,
602-
(_, _) => 1,
603-
};
604-
605-
FormatEmptyLinesAfterLeadingComments {
606-
comments,
607-
empty_lines,
608-
}
591+
pub(crate) fn empty_lines_after_leading_comments(
592+
comments: &[SourceComment],
593+
) -> FormatEmptyLinesAfterLeadingComments {
594+
FormatEmptyLinesAfterLeadingComments { comments }
609595
}
610596

611597
#[derive(Copy, Clone, Debug)]
612598
pub(crate) struct FormatEmptyLinesAfterLeadingComments<'a> {
613599
/// The leading comments of the node.
614600
comments: &'a [SourceComment],
615-
/// The expected number of empty lines after the leading comments.
616-
empty_lines: u32,
617601
}
618602

619603
impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
@@ -624,6 +608,14 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
624608
.rev()
625609
.find(|comment| comment.line_position().is_own_line())
626610
{
611+
// Black has different rules for stub vs. non-stub and top level vs. indented
612+
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
613+
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
614+
(PySourceType::Stub, _) => 0,
615+
(_, NodeLevel::TopLevel(_)) => 2,
616+
(_, _) => 1,
617+
};
618+
627619
let actual = lines_after(comment.end(), f.context().source()).saturating_sub(1);
628620
// If there are no empty lines, keep the comment tight to the node.
629621
if actual == 0 {
@@ -632,12 +624,12 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
632624

633625
// If there are more than enough empty lines already, `leading_comments` will
634626
// trim them as necessary.
635-
if actual >= self.empty_lines {
627+
if actual >= empty_lines {
636628
return Ok(());
637629
}
638630

639-
for _ in actual..self.empty_lines {
640-
write!(f, [empty_line()])?;
631+
for _ in actual..empty_lines {
632+
empty_line().fmt(f)?;
641633
}
642634
}
643635
Ok(())

crates/ruff_python_formatter/src/statement/stmt_class_def.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
5555
// newline between the comment and the node, but we _require_ two newlines. If there are
5656
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
5757
// are more than two, then `leading_comments` will preserve the correct number of newlines.
58-
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
58+
empty_lines_after_leading_comments(comments.leading(item)).fmt(f)?;
5959

6060
write!(
6161
f,
@@ -152,7 +152,7 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
152152
//
153153
// # comment
154154
// ```
155-
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtClassDef)
155+
empty_lines_before_trailing_comments(comments.trailing(item), NodeKind::StmtClassDef)
156156
.fmt(f)?;
157157

158158
Ok(())

crates/ruff_python_formatter/src/statement/stmt_function_def.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
5252
// newline between the comment and the node, but we _require_ two newlines. If there are
5353
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
5454
// are more than two, then `leading_comments` will preserve the correct number of newlines.
55-
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
55+
empty_lines_after_leading_comments(comments.leading(item)).fmt(f)?;
5656

5757
write!(
5858
f,
@@ -86,7 +86,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
8686
//
8787
// # comment
8888
// ```
89-
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtFunctionDef)
89+
empty_lines_before_trailing_comments(comments.trailing(item), NodeKind::StmtFunctionDef)
9090
.fmt(f)
9191
}
9292
}

crates/ruff_python_formatter/src/statement/suite.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
240240
preceding_stub.end(),
241241
f.context().source(),
242242
) < 2
243-
});
243+
})
244+
&& !preceding_comments.has_trailing_own_line();
244245

245246
if !is_preceding_stub_function_without_empty_line {
246247
match self.kind {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
source: crates/ruff_python_formatter/tests/fixtures.rs
3+
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/stub_functions_trailing_comments.py
4+
---
5+
## Input
6+
```python
7+
# Regression tests for https://github.com/astral-sh/ruff/issues/11569
8+
9+
10+
# comment 1
11+
def foo(self) -> None: ...
12+
def bar(self) -> None: ...
13+
# comment 2
14+
15+
# comment 3
16+
def baz(self) -> None:
17+
return None
18+
# comment 4
19+
20+
21+
def foo(self) -> None: ...
22+
# comment 5
23+
24+
def baz(self) -> None:
25+
return None
26+
27+
28+
def foo(self) -> None:
29+
... # comment 5
30+
def baz(self) -> None:
31+
return None
32+
33+
def foo(self) -> None: ...
34+
# comment 5
35+
```
36+
37+
## Output
38+
```python
39+
# Regression tests for https://github.com/astral-sh/ruff/issues/11569
40+
41+
42+
# comment 1
43+
def foo(self) -> None: ...
44+
def bar(self) -> None: ...
45+
46+
47+
# comment 2
48+
49+
50+
# comment 3
51+
def baz(self) -> None:
52+
return None
53+
54+
55+
# comment 4
56+
57+
58+
def foo(self) -> None: ...
59+
60+
61+
# comment 5
62+
63+
64+
def baz(self) -> None:
65+
return None
66+
67+
68+
def foo(self) -> None: ... # comment 5
69+
def baz(self) -> None:
70+
return None
71+
72+
73+
def foo(self) -> None: ...
74+
75+
76+
# comment 5
77+
```

0 commit comments

Comments
 (0)