Skip to content

Commit 7809a3c

Browse files
committed
[pylint] - use sets when possible for PLR1714 autofix
1 parent c847cad commit 7809a3c

File tree

3 files changed

+156
-50
lines changed

3 files changed

+156
-50
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,11 @@
6565
foo == "a" or ("c" != bar and "d" != bar) or foo == "b" # Multiple targets
6666

6767
foo == "a" and "c" != bar or foo == "b" and "d" != bar # Multiple targets
68+
69+
foo == 1 or foo == True # Different types, same hashed value, Tuple expected
70+
71+
foo == 1 or foo == 1.0 # Different types, same hashed value, Tuple expected
72+
73+
foo == False or foo == 0 # Different types, same hashed value, Tuple expected
74+
75+
foo == 0.0 or foo == 0j # Different types, same hashed value, Tuple expected

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use itertools::Itertools;
2-
use rustc_hash::{FxBuildHasher, FxHashMap};
2+
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
33

44
use ast::ExprContext;
55
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
66
use ruff_macros::{derive_message_formats, violation};
7-
use ruff_python_ast::comparable::ComparableExpr;
7+
use ruff_python_ast::comparable::{ComparableExpr, HashableExpr};
88
use ruff_python_ast::helpers::{any_over_expr, contains_effect};
99
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
1010
use ruff_python_semantic::SemanticModel;
@@ -51,12 +51,9 @@ impl AlwaysFixableViolation for RepeatedEqualityComparison {
5151
#[derive_message_formats]
5252
fn message(&self) -> String {
5353
if let Some(expression) = self.expression.full_display() {
54-
format!(
55-
"Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable."
56-
)
54+
format!("Consider merging multiple comparisons: `{expression}`.")
5755
} else {
58-
"Consider merging multiple comparisons. Use a `set` if the elements are hashable."
59-
.to_string()
56+
"Consider merging multiple comparisons.".to_string()
6057
}
6158
}
6259

@@ -121,13 +118,20 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
121118
continue;
122119
}
123120

121+
let mut seen_values =
122+
FxHashSet::with_capacity_and_hasher(comparators.len(), FxBuildHasher);
123+
let use_set = comparators
124+
.iter()
125+
.all(|comparator| seen_values.insert(HashableExpr::from(*comparator)));
126+
124127
let mut diagnostic = Diagnostic::new(
125128
RepeatedEqualityComparison {
126129
expression: SourceCodeSnippet::new(merged_membership_test(
127130
expr,
128131
bool_op.op,
129132
&comparators,
130133
checker.locator(),
134+
use_set,
131135
)),
132136
},
133137
bool_op.range(),
@@ -140,6 +144,20 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
140144
let before = bool_op.values.iter().take(*first).cloned();
141145
let after = bool_op.values.iter().skip(last + 1).cloned();
142146

147+
let comparator = if use_set {
148+
Expr::Set(ast::ExprSet {
149+
elts: comparators.iter().copied().cloned().collect(),
150+
range: TextRange::default(),
151+
})
152+
} else {
153+
Expr::Tuple(ast::ExprTuple {
154+
elts: comparators.iter().copied().cloned().collect(),
155+
range: TextRange::default(),
156+
ctx: ExprContext::Load,
157+
parenthesized: true,
158+
})
159+
};
160+
143161
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
144162
checker.generator().expr(&Expr::BoolOp(ast::ExprBoolOp {
145163
op: bool_op.op,
@@ -150,12 +168,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
150168
BoolOp::Or => Box::from([CmpOp::In]),
151169
BoolOp::And => Box::from([CmpOp::NotIn]),
152170
},
153-
comparators: Box::from([Expr::Tuple(ast::ExprTuple {
154-
elts: comparators.iter().copied().cloned().collect(),
155-
range: TextRange::default(),
156-
ctx: ExprContext::Load,
157-
parenthesized: true,
158-
})]),
171+
comparators: Box::from([comparator]),
159172
range: bool_op.range(),
160173
})))
161174
.chain(after)
@@ -231,11 +244,13 @@ fn to_allowed_value<'a>(
231244
}
232245

233246
/// Generate a string like `obj in (a, b, c)` or `obj not in (a, b, c)`.
247+
/// If `use_set` is `true`, the string will use a set instead of a tuple.
234248
fn merged_membership_test(
235249
left: &Expr,
236250
op: BoolOp,
237251
comparators: &[&Expr],
238252
locator: &Locator,
253+
use_set: bool,
239254
) -> String {
240255
let op = match op {
241256
BoolOp::Or => "in",
@@ -246,5 +261,10 @@ fn merged_membership_test(
246261
.iter()
247262
.map(|comparator| locator.slice(comparator))
248263
.join(", ");
264+
265+
if use_set {
266+
return format!("{left} {op} {{{members}}}",);
267+
}
268+
249269
format!("{left} {op} ({members})",)
250270
}

0 commit comments

Comments
 (0)