Skip to content

Commit 764d9ab

Browse files
Allow repeated-equality-comparison for mixed operations (#12369)
## Summary This PR allows us to fix both expressions in `foo == "a" or foo == "b" or ("c" != bar and "d" != bar)`, but limits the rule to consecutive comparisons, following #7797. I think this logic was _probably_ added because of #12368 -- the intent being that we'd replace the _entire_ expression.
1 parent 9b9d701 commit 764d9ab

File tree

4 files changed

+102
-87
lines changed

4 files changed

+102
-87
lines changed

crates/ruff_linter/resources/test/fixtures/pylint/repeated_equality_comparison.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@
6161
foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
6262

6363
foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
64+
65+
foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
66+
67+
foo == "a" and "c" != bar or foo == "b" and "d" != bar # Multiple targets

crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::ops::Deref;
2-
31
use itertools::Itertools;
42
use rustc_hash::{FxBuildHasher, FxHashMap};
53

@@ -72,79 +70,83 @@ impl AlwaysFixableViolation for RepeatedEqualityComparison {
7270

7371
/// PLR1714
7472
pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast::ExprBoolOp) {
75-
if bool_op
76-
.values
77-
.iter()
78-
.any(|value| !is_allowed_value(bool_op.op, value, checker.semantic()))
79-
{
80-
return;
81-
}
82-
8373
// Map from expression hash to (starting offset, number of comparisons, list
84-
let mut value_to_comparators: FxHashMap<HashableExpr, (TextSize, Vec<&Expr>, Vec<&Expr>)> =
74+
let mut value_to_comparators: FxHashMap<HashableExpr, (TextSize, Vec<&Expr>, Vec<usize>)> =
8575
FxHashMap::with_capacity_and_hasher(bool_op.values.len() * 2, FxBuildHasher);
8676

87-
for value in &bool_op.values {
88-
// Enforced via `is_allowed_value`.
89-
let Expr::Compare(ast::ExprCompare {
90-
left, comparators, ..
91-
}) = value
92-
else {
93-
return;
94-
};
95-
96-
// Enforced via `is_allowed_value`.
97-
let [right] = &**comparators else {
98-
return;
77+
for (i, value) in bool_op.values.iter().enumerate() {
78+
let Some((left, right)) = to_allowed_value(bool_op.op, value, checker.semantic()) else {
79+
continue;
9980
};
10081

101-
if matches!(left.as_ref(), Expr::Name(_) | Expr::Attribute(_)) {
102-
let (_, left_matches, value_matches) = value_to_comparators
103-
.entry(left.deref().into())
82+
if matches!(left, Expr::Name(_) | Expr::Attribute(_)) {
83+
let (_, left_matches, index_matches) = value_to_comparators
84+
.entry(left.into())
10485
.or_insert_with(|| (left.start(), Vec::new(), Vec::new()));
10586
left_matches.push(right);
106-
value_matches.push(value);
87+
index_matches.push(i);
10788
}
10889

10990
if matches!(right, Expr::Name(_) | Expr::Attribute(_)) {
110-
let (_, right_matches, value_matches) = value_to_comparators
91+
let (_, right_matches, index_matches) = value_to_comparators
11192
.entry(right.into())
11293
.or_insert_with(|| (right.start(), Vec::new(), Vec::new()));
11394
right_matches.push(left);
114-
value_matches.push(value);
95+
index_matches.push(i);
11596
}
11697
}
11798

118-
for (value, (start, comparators, values)) in value_to_comparators
99+
for (value, (_, comparators, indices)) in value_to_comparators
119100
.iter()
120101
.sorted_by_key(|(_, (start, _, _))| *start)
121102
{
122-
if comparators.len() > 1 {
103+
// If there's only one comparison, there's nothing to merge.
104+
if comparators.len() == 1 {
105+
continue;
106+
}
107+
108+
// Break into sequences of consecutive comparisons.
109+
let mut sequences: Vec<(Vec<usize>, Vec<&Expr>)> = Vec::new();
110+
let mut last = None;
111+
for (index, comparator) in indices.iter().zip(comparators.iter()) {
112+
if last.is_some_and(|last| last + 1 == *index) {
113+
let (indices, comparators) = sequences.last_mut().unwrap();
114+
indices.push(*index);
115+
comparators.push(*comparator);
116+
} else {
117+
sequences.push((vec![*index], vec![*comparator]));
118+
}
119+
last = Some(*index);
120+
}
121+
122+
for (indices, comparators) in sequences {
123+
if indices.len() == 1 {
124+
continue;
125+
}
126+
123127
let mut diagnostic = Diagnostic::new(
124128
RepeatedEqualityComparison {
125129
expression: SourceCodeSnippet::new(merged_membership_test(
126130
value.as_expr(),
127131
bool_op.op,
128-
comparators,
132+
&comparators,
129133
checker.locator(),
130134
)),
131135
},
132136
bool_op.range(),
133137
);
134138

135139
// Grab the remaining comparisons.
136-
let (before, after) = bool_op
137-
.values
138-
.iter()
139-
.filter(|value| !values.contains(value))
140-
.partition::<Vec<_>, _>(|value| value.start() < *start);
140+
let [first, .., last] = indices.as_slice() else {
141+
unreachable!("Indices should have at least two elements")
142+
};
143+
let before = bool_op.values.iter().take(*first).cloned();
144+
let after = bool_op.values.iter().skip(last + 1).cloned();
141145

142146
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
143147
checker.generator().expr(&Expr::BoolOp(ast::ExprBoolOp {
144148
op: bool_op.op,
145149
values: before
146-
.into_iter()
147-
.cloned()
148150
.chain(std::iter::once(Expr::Compare(ast::ExprCompare {
149151
left: Box::new(value.as_expr().clone()),
150152
ops: match bool_op.op {
@@ -159,7 +161,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
159161
})]),
160162
range: bool_op.range(),
161163
})))
162-
.chain(after.into_iter().cloned())
164+
.chain(after)
163165
.collect(),
164166
range: bool_op.range(),
165167
})),
@@ -174,39 +176,43 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
174176
/// Return `true` if the given expression is compatible with a membership test.
175177
/// E.g., `==` operators can be joined with `or` and `!=` operators can be
176178
/// joined with `and`.
177-
fn is_allowed_value(bool_op: BoolOp, value: &Expr, semantic: &SemanticModel) -> bool {
179+
fn to_allowed_value<'a>(
180+
bool_op: BoolOp,
181+
value: &'a Expr,
182+
semantic: &SemanticModel,
183+
) -> Option<(&'a Expr, &'a Expr)> {
178184
let Expr::Compare(ast::ExprCompare {
179185
left,
180186
ops,
181187
comparators,
182188
..
183189
}) = value
184190
else {
185-
return false;
191+
return None;
186192
};
187193

188194
// Ignore, e.g., `foo == bar == baz`.
189195
let [op] = &**ops else {
190-
return false;
196+
return None;
191197
};
192198

193199
if match bool_op {
194200
BoolOp::Or => !matches!(op, CmpOp::Eq),
195201
BoolOp::And => !matches!(op, CmpOp::NotEq),
196202
} {
197-
return false;
203+
return None;
198204
}
199205

200206
// Ignore self-comparisons, e.g., `foo == foo`.
201207
let [right] = &**comparators else {
202-
return false;
208+
return None;
203209
};
204210
if ComparableExpr::from(left) == ComparableExpr::from(right) {
205-
return false;
211+
return None;
206212
}
207213

208214
if contains_effect(value, |id| semantic.has_builtin_binding(id)) {
209-
return false;
215+
return None;
210216
}
211217

212218
// Ignore `sys.version_info` and `sys.platform` comparisons, which are only
@@ -221,10 +227,10 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr, semantic: &SemanticModel) ->
221227
)
222228
})
223229
}) {
224-
return false;
230+
return None;
225231
}
226232

227-
true
233+
Some((left, right))
228234
}
229235

230236
/// Generate a string like `obj in (a, b, c)` or `obj not in (a, b, c)`.

crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub struct RepeatedIsinstanceCalls {
5353
expression: SourceCodeSnippet,
5454
}
5555

56-
// PLR1701
56+
/// PLR1701
5757
impl AlwaysFixableViolation for RepeatedIsinstanceCalls {
5858
#[derive_message_formats]
5959
fn message(&self) -> String {

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -292,80 +292,85 @@ repeated_equality_comparison.py:26:1: PLR1714 [*] Consider merging multiple comp
292292
28 28 | # OK
293293
29 29 | foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
294294

295-
repeated_equality_comparison.py:59:1: PLR1714 [*] Consider merging multiple comparisons: `foo in ("a", "b")`. Use a `set` if the elements are hashable.
295+
repeated_equality_comparison.py:61:16: PLR1714 [*] Consider merging multiple comparisons: `bar in ("c", "d")`. Use a `set` if the elements are hashable.
296296
|
297-
57 | sys.platform == "win32" or sys.platform == "emscripten" # sys attributes
298-
58 |
299297
59 | foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
300-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
301298
60 |
302299
61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
300+
| ^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
301+
62 |
302+
63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
303303
|
304304
= help: Merge multiple comparisons
305305

306306
Unsafe fix
307-
56 56 |
308-
57 57 | sys.platform == "win32" or sys.platform == "emscripten" # sys attributes
309307
58 58 |
310-
59 |-foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
311-
59 |+foo in ("a", "b") or "c" == bar or "d" == bar # Multiple targets
308+
59 59 | foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
312309
60 60 |
313-
61 61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
310+
61 |-foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
311+
61 |+foo == "a" or (bar in ("c", "d")) or foo == "b" # Multiple targets
314312
62 62 |
313+
63 63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
314+
64 64 |
315315

316-
repeated_equality_comparison.py:59:1: PLR1714 [*] Consider merging multiple comparisons: `bar in ("c", "d")`. Use a `set` if the elements are hashable.
316+
repeated_equality_comparison.py:63:1: PLR1714 [*] Consider merging multiple comparisons: `foo in ("a", "b")`. Use a `set` if the elements are hashable.
317317
|
318-
57 | sys.platform == "win32" or sys.platform == "emscripten" # sys attributes
319-
58 |
320-
59 | foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
321-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
322-
60 |
323318
61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
319+
62 |
320+
63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
321+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
322+
64 |
323+
65 | foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
324324
|
325325
= help: Merge multiple comparisons
326326

327327
Unsafe fix
328-
56 56 |
329-
57 57 | sys.platform == "win32" or sys.platform == "emscripten" # sys attributes
330-
58 58 |
331-
59 |-foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
332-
59 |+foo == "a" or bar in ("c", "d") or foo == "b" # Multiple targets
333328
60 60 |
334329
61 61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
335330
62 62 |
331+
63 |-foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
332+
63 |+foo in ("a", "b") or "c" != bar and "d" != bar # Multiple targets
333+
64 64 |
334+
65 65 | foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
335+
66 66 |
336336

337-
repeated_equality_comparison.py:61:16: PLR1714 [*] Consider merging multiple comparisons: `bar in ("c", "d")`. Use a `set` if the elements are hashable.
337+
repeated_equality_comparison.py:63:29: PLR1714 [*] Consider merging multiple comparisons: `bar not in ("c", "d")`. Use a `set` if the elements are hashable.
338338
|
339-
59 | foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
340-
60 |
341339
61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
342-
| ^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
343340
62 |
344341
63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
342+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
343+
64 |
344+
65 | foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
345345
|
346346
= help: Merge multiple comparisons
347347

348348
Unsafe fix
349-
58 58 |
350-
59 59 | foo == "a" or "c" == bar or foo == "b" or "d" == bar # Multiple targets
351349
60 60 |
352-
61 |-foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
353-
61 |+foo == "a" or (bar in ("c", "d")) or foo == "b" # Multiple targets
350+
61 61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
354351
62 62 |
355-
63 63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
352+
63 |-foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
353+
63 |+foo == "a" or foo == "b" or bar not in ("c", "d") # Multiple targets
354+
64 64 |
355+
65 65 | foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
356+
66 66 |
356357

357-
repeated_equality_comparison.py:63:29: PLR1714 [*] Consider merging multiple comparisons: `bar not in ("c", "d")`. Use a `set` if the elements are hashable.
358+
repeated_equality_comparison.py:65:16: PLR1714 [*] Consider merging multiple comparisons: `bar not in ("c", "d")`. Use a `set` if the elements are hashable.
358359
|
359-
61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
360-
62 |
361360
63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
362-
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
361+
64 |
362+
65 | foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
363+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714
364+
66 |
365+
67 | foo == "a" and "c" != bar or foo == "b" and "d" != bar # Multiple targets
363366
|
364367
= help: Merge multiple comparisons
365368

366369
Unsafe fix
367-
60 60 |
368-
61 61 | foo == "a" or ("c" == bar or "d" == bar) or foo == "b" # Multiple targets
369370
62 62 |
370-
63 |-foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
371-
63 |+foo == "a" or foo == "b" or bar not in ("c", "d") # Multiple targets
371+
63 63 | foo == "a" or foo == "b" or "c" != bar and "d" != bar # Multiple targets
372+
64 64 |
373+
65 |-foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
374+
65 |+foo == "a" or (bar not in ("c", "d")) or foo == "b" # Multiple targets
375+
66 66 |
376+
67 67 | foo == "a" and "c" != bar or foo == "b" and "d" != bar # Multiple targets

0 commit comments

Comments
 (0)