Skip to content

Commit 8f9753f

Browse files
konstinMichaReiser
andauthored
Comments outside expression parentheses (#7873)
<!-- 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 Fixes #7448 Fixes #7892 I've removed automatic dangling comment formatting, we're doing manual dangling comment formatting everywhere anyway (the assert-all-comments-formatted ensures this) and dangling comments would break the formatting there. ## Test Plan New test file. --------- Co-authored-by: Micha Reiser <[email protected]>
1 parent 67b0434 commit 8f9753f

File tree

13 files changed

+652
-124
lines changed

13 files changed

+652
-124
lines changed

crates/ruff_formatter/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,10 @@ where
575575
context: PhantomData,
576576
}
577577
}
578+
579+
pub fn rule(&self) -> &R {
580+
&self.rule
581+
}
578582
}
579583

580584
impl<T, R, O, C> FormatRefWithRule<'_, T, R, C>

crates/ruff_python_ast/src/parenthesize.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,44 @@ use ruff_text_size::{Ranged, TextLen, TextRange};
44
use crate::AnyNodeRef;
55
use crate::ExpressionRef;
66

7-
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
8-
/// parenthesized; or `None`, if the expression is not parenthesized.
9-
pub fn parenthesized_range(
10-
expr: ExpressionRef,
11-
parent: AnyNodeRef,
12-
comment_ranges: &CommentRanges,
13-
source: &str,
14-
) -> Option<TextRange> {
15-
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
16-
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
17-
// the open and close parentheses are part of the `Arguments` node.
18-
//
19-
// There are a few other nodes that may have their own parentheses, but are fine to exclude:
20-
// - `Parameters`: The parameters to a function definition. Any expressions would represent
21-
// default arguments, and so must be preceded by _at least_ the parameter name. As such,
22-
// we won't mistake any parentheses for the opening and closing parentheses on the
23-
// `Parameters` node itself.
24-
// - `Tuple`: The elements of a tuple. The only risk is a single-element tuple (e.g., `(x,)`),
25-
// which must have a trailing comma anyway.
26-
let exclusive_parent_end = if parent.is_arguments() {
27-
parent.end() - ")".text_len()
7+
/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.
8+
///
9+
/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7).
10+
///
11+
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
12+
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
13+
/// generally prefer [`parenthesized_range`].
14+
pub fn parentheses_iterator<'a>(
15+
expr: ExpressionRef<'a>,
16+
parent: Option<AnyNodeRef>,
17+
comment_ranges: &'a CommentRanges,
18+
source: &'a str,
19+
) -> impl Iterator<Item = TextRange> + 'a {
20+
let right_tokenizer = if let Some(parent) = parent {
21+
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
22+
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
23+
// the open and close parentheses are part of the `Arguments` node.
24+
//
25+
// There are a few other nodes that may have their own parentheses, but are fine to exclude:
26+
// - `Parameters`: The parameters to a function definition. Any expressions would represent
27+
// default arguments, and so must be preceded by _at least_ the parameter name. As such,
28+
// we won't mistake any parentheses for the opening and closing parentheses on the
29+
// `Parameters` node itself.
30+
// - `Tuple`: The elements of a tuple. The only risk is a single-element tuple (e.g., `(x,)`),
31+
// which must have a trailing comma anyway.
32+
let exclusive_parent_end = if parent.is_arguments() {
33+
parent.end() - ")".text_len()
34+
} else {
35+
parent.end()
36+
};
37+
SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
2838
} else {
29-
parent.end()
39+
SimpleTokenizer::starts_at(expr.end(), source)
3040
};
3141

32-
let right_tokenizer =
33-
SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
34-
.skip_trivia()
35-
.take_while(|token| token.kind == SimpleTokenKind::RParen);
42+
let right_tokenizer = right_tokenizer
43+
.skip_trivia()
44+
.take_while(|token| token.kind == SimpleTokenKind::RParen);
3645

3746
let left_tokenizer = BackwardsTokenizer::up_to(expr.start(), source, comment_ranges)
3847
.skip_trivia()
@@ -43,6 +52,16 @@ pub fn parenthesized_range(
4352
// the `right_tokenizer` is exhausted.
4453
right_tokenizer
4554
.zip(left_tokenizer)
46-
.last()
4755
.map(|(right, left)| TextRange::new(left.start(), right.end()))
4856
}
57+
58+
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
59+
/// parenthesized; or `None`, if the expression is not parenthesized.
60+
pub fn parenthesized_range(
61+
expr: ExpressionRef,
62+
parent: AnyNodeRef,
63+
comment_ranges: &CommentRanges,
64+
source: &str,
65+
) -> Option<TextRange> {
66+
parentheses_iterator(expr, Some(parent), comment_ranges, source).last()
67+
}

crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,14 @@
161161
+ "WARNING: Removing listed files. Do you really want to continue. yes/n)? "
162162
):
163163
pass
164+
165+
# https://github.com/astral-sh/ruff/issues/7448
166+
x = (
167+
# a
168+
not # b
169+
# c
170+
( # d
171+
# e
172+
True
173+
)
174+
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
list_with_parenthesized_elements1 = [
2+
# comment leading outer
3+
(
4+
# comment leading inner
5+
1 + 2 # comment trailing inner
6+
) # comment trailing outer
7+
]
8+
9+
list_with_parenthesized_elements2 = [
10+
# leading outer
11+
(1 + 2)
12+
]
13+
list_with_parenthesized_elements3 = [
14+
# leading outer
15+
(1 + 2) # trailing outer
16+
]
17+
list_with_parenthesized_elements4 = [
18+
# leading outer
19+
(1 + 2), # trailing outer
20+
]
21+
list_with_parenthesized_elements5 = [
22+
(1), # trailing outer
23+
(2), # trailing outer
24+
]
25+
26+
nested_parentheses1 = (
27+
(
28+
(
29+
1
30+
) # i
31+
) # j
32+
) # k
33+
nested_parentheses2 = [
34+
(
35+
(
36+
(
37+
1
38+
) # i
39+
# i2
40+
) # j
41+
# j2
42+
) # k
43+
# k2
44+
]
45+
nested_parentheses3 = (
46+
( # a
47+
( # b
48+
1
49+
) # i
50+
) # j
51+
) # k
52+
nested_parentheses4 = [
53+
# a
54+
( # b
55+
# c
56+
( # d
57+
# e
58+
( #f
59+
1
60+
) # i
61+
# i2
62+
) # j
63+
# j2
64+
) # k
65+
# k2
66+
]
67+
68+
69+
x = (
70+
# unary comment
71+
not
72+
# in-between comment
73+
(
74+
# leading inner
75+
"a"
76+
),
77+
not # in-between comment
78+
(
79+
# leading inner
80+
"b"
81+
),
82+
not
83+
( # in-between comment
84+
# leading inner
85+
"c"
86+
),
87+
# 1
88+
not # 2
89+
( # 3
90+
# 4
91+
"d"
92+
)
93+
)
94+
95+
if (
96+
# unary comment
97+
not
98+
# in-between comment
99+
(
100+
# leading inner
101+
1
102+
)
103+
):
104+
pass
105+
106+
# Make sure we keep a inside the parentheses
107+
# https://github.com/astral-sh/ruff/issues/7892
108+
x = (
109+
# a
110+
( # b
111+
1
112+
)
113+
)

crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@
8686
)
8787
): pass
8888

89-
with (a # trailing same line comment
90-
# trailing own line comment
91-
) as b: pass
92-
9389
with (
9490
a # trailing same line comment
9591
# trailing own line comment

crates/ruff_python_formatter/src/comments/placement.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,8 +1878,7 @@ fn handle_lambda_comment<'a>(
18781878
CommentPlacement::Default(comment)
18791879
}
18801880

1881-
/// Attach trailing end-of-line comments on the operator as dangling comments on the enclosing
1882-
/// node.
1881+
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
18831882
///
18841883
/// For example, given:
18851884
/// ```python
@@ -1896,26 +1895,27 @@ fn handle_unary_op_comment<'a>(
18961895
unary_op: &'a ast::ExprUnaryOp,
18971896
locator: &Locator,
18981897
) -> CommentPlacement<'a> {
1899-
if comment.line_position().is_own_line() {
1900-
return CommentPlacement::Default(comment);
1901-
}
1902-
1903-
if comment.start() > unary_op.operand.start() {
1904-
return CommentPlacement::Default(comment);
1905-
}
1906-
1907-
let tokenizer = SimpleTokenizer::new(
1898+
let mut tokenizer = SimpleTokenizer::new(
19081899
locator.contents(),
1909-
TextRange::new(comment.start(), unary_op.operand.start()),
1910-
);
1911-
if tokenizer
1912-
.skip_trivia()
1913-
.any(|token| token.kind == SimpleTokenKind::LParen)
1914-
{
1915-
return CommentPlacement::Default(comment);
1900+
TextRange::new(unary_op.start(), unary_op.operand.start()),
1901+
)
1902+
.skip_trivia();
1903+
let op_token = tokenizer.next();
1904+
debug_assert!(op_token.is_some_and(|token| matches!(
1905+
token.kind,
1906+
SimpleTokenKind::Tilde
1907+
| SimpleTokenKind::Not
1908+
| SimpleTokenKind::Plus
1909+
| SimpleTokenKind::Minus
1910+
)));
1911+
let up_to = tokenizer
1912+
.find(|token| token.kind == SimpleTokenKind::LParen)
1913+
.map_or(unary_op.operand.start(), |lparen| lparen.start());
1914+
if comment.end() < up_to {
1915+
CommentPlacement::leading(unary_op, comment)
1916+
} else {
1917+
CommentPlacement::Default(comment)
19161918
}
1917-
1918-
CommentPlacement::dangling(comment.enclosing_node(), comment)
19191919
}
19201920

19211921
/// Attach an end-of-line comment immediately following an open bracket as a dangling comment on

0 commit comments

Comments
 (0)