Skip to content

Commit 1e6df19

Browse files
authored
Bool expression comment placement (#7269)
1 parent c21b960 commit 1e6df19

File tree

12 files changed

+776
-145
lines changed

12 files changed

+776
-145
lines changed

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,14 @@
320320
(1 << (n + 2*n-1 + i+j)) # NE-SW ordinal
321321
for j in rangen]
322322

323+
rowuses = [((1 << j) # column ordinal
324+
)|
325+
(
326+
# comment
327+
(1 << (n + i-j + n-1))) | # NW-SE ordinal
328+
(1 << (n + 2*n-1 + i+j)) # NE-SW ordinal
329+
for j in rangen]
330+
323331
skip_bytes = (
324332
header.timecnt * 5 # Transition times and types
325333
+ header.typecnt * 6 # Local time type records
@@ -328,3 +336,56 @@
328336
+ header.isstdcnt # Standard/wall indicators
329337
+ header.isutcnt # UT/local indicators
330338
)
339+
340+
341+
if (
342+
(1 + 2) # test
343+
or (3 + 4) # other
344+
or (4 + 5) # more
345+
):
346+
pass
347+
348+
349+
if (
350+
(1 and 2) # test
351+
+ (3 and 4) # other
352+
+ (4 and 5) # more
353+
):
354+
pass
355+
356+
357+
if (
358+
(1 + 2) # test
359+
< (3 + 4) # other
360+
> (4 + 5) # more
361+
):
362+
pass
363+
364+
z = (
365+
a
366+
+
367+
# a: extracts this comment
368+
(
369+
# b: and this comment
370+
(
371+
# c: formats it as part of the expression
372+
x and y
373+
)
374+
)
375+
)
376+
377+
z = (
378+
(
379+
380+
(
381+
382+
x and y
383+
# a: formats it as part of the expression
384+
385+
)
386+
# b: extracts this comment
387+
388+
)
389+
# c: and this comment
390+
+ a
391+
)

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,86 @@ def test():
102102
and {k.lower(): v for k, v in self.items()}
103103
== {k.lower(): v for k, v in other.items()}
104104
)
105+
106+
107+
108+
if "_continue" in request.POST or (
109+
# Redirecting after "Save as new".
110+
"_saveasnew" in request.POST
111+
and self.save_as_continue
112+
and self.has_change_permission(request, obj)
113+
):
114+
pass
115+
116+
117+
if True:
118+
if False:
119+
if True:
120+
if (
121+
self.validate_max
122+
and self.total_form_count() - len(self.deleted_forms) > self.max_num
123+
) or self.management_form.cleaned_data[
124+
TOTAL_FORM_COUNT
125+
] > self.absolute_max:
126+
pass
127+
128+
129+
if True:
130+
if (
131+
reference_field_name is None
132+
or
133+
# Unspecified to_field(s).
134+
to_fields is None
135+
or
136+
# Reference to primary key.
137+
(
138+
None in to_fields
139+
and (reference_field is None or reference_field.primary_key)
140+
)
141+
or
142+
# Reference to field.
143+
reference_field_name in to_fields
144+
):
145+
pass
146+
147+
148+
field = opts.get_field(name)
149+
if (
150+
field.is_relation
151+
and
152+
# Generic foreign keys OR reverse relations
153+
((field.many_to_one and not field.related_model) or field.one_to_many)
154+
):
155+
pass
156+
157+
158+
if True:
159+
return (
160+
filtered.exists()
161+
and
162+
# It may happen that the object is deleted from the DB right after
163+
# this check, causing the subsequent UPDATE to return zero matching
164+
# rows. The same result can occur in some rare cases when the
165+
# database returns zero despite the UPDATE being executed
166+
# successfully (a row is matched and updated). In order to
167+
# distinguish these two cases, the object's existence in the
168+
# database is again checked for if the UPDATE query returns 0.
169+
(filtered._update(values) > 0 or filtered.exists())
170+
)
171+
172+
173+
if (self._proc is not None
174+
# has the child process finished?
175+
and self._returncode is None
176+
# the child process has finished, but the
177+
# transport hasn't been notified yet?
178+
and self._proc.poll() is None):
179+
pass
180+
181+
if (self._proc
182+
# has the child process finished?
183+
* self._returncode
184+
# the child process has finished, but the
185+
# transport hasn't been notified yet?
186+
+ self._proc.poll()):
187+
pass

crates/ruff_python_formatter/src/comments/placement.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ fn handle_enclosed_comment<'a>(
205205
locator,
206206
)
207207
}
208+
AnyNodeRef::ExprBoolOp(_) | AnyNodeRef::ExprCompare(_) => {
209+
handle_trailing_binary_like_comment(comment, locator)
210+
}
208211
AnyNodeRef::Keyword(keyword) => handle_keyword_comment(comment, keyword, locator),
209212
AnyNodeRef::PatternKeyword(pattern_keyword) => {
210213
handle_pattern_keyword_comment(comment, pattern_keyword, locator)
@@ -836,6 +839,47 @@ fn handle_trailing_binary_expression_left_or_operator_comment<'a>(
836839
}
837840
}
838841

842+
/// Attaches comments between two bool or compare expression operands to the preceding operand if the comment is before the operator.
843+
///
844+
/// ```python
845+
/// a = (
846+
/// 5 > 3
847+
/// # trailing comment
848+
/// and 3 == 3
849+
/// )
850+
/// ```
851+
fn handle_trailing_binary_like_comment<'a>(
852+
comment: DecoratedComment<'a>,
853+
locator: &Locator,
854+
) -> CommentPlacement<'a> {
855+
debug_assert!(
856+
comment.enclosing_node().is_expr_bool_op() || comment.enclosing_node().is_expr_compare()
857+
);
858+
859+
// Only if there's a preceding node (in which case, the preceding node is `left` or middle node).
860+
let (Some(left_operand), Some(right_operand)) =
861+
(comment.preceding_node(), comment.following_node())
862+
else {
863+
return CommentPlacement::Default(comment);
864+
};
865+
866+
let between_operands_range = TextRange::new(left_operand.end(), right_operand.start());
867+
868+
let mut tokens = SimpleTokenizer::new(locator.contents(), between_operands_range)
869+
.skip_trivia()
870+
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
871+
let operator_offset = tokens
872+
.next()
873+
.expect("Expected a token for the operator")
874+
.start();
875+
876+
if comment.end() < operator_offset {
877+
CommentPlacement::trailing(left_operand, comment)
878+
} else {
879+
CommentPlacement::Default(comment)
880+
}
881+
}
882+
839883
/// Handles own line comments on the module level before a class or function statement.
840884
/// A comment only becomes the leading comment of a class or function if it isn't separated by an empty
841885
/// line from the class. Comments that are separated by at least one empty line from the header of the

0 commit comments

Comments
 (0)