Skip to content

Commit 4b47271

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

File tree

3 files changed

+151
-44
lines changed

3 files changed

+151
-44
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: 32 additions & 11 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,9 +51,7 @@ 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}`. Use a `set` if the elements are hashable.")
5755
} else {
5856
"Consider merging multiple comparisons. Use a `set` if the elements are hashable."
5957
.to_string()
@@ -121,13 +119,20 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
121119
continue;
122120
}
123121

122+
let mut seen_values =
123+
FxHashSet::with_capacity_and_hasher(comparators.len(), FxBuildHasher);
124+
let use_set = comparators.iter().all(|comparator| {
125+
comparator.is_literal_expr() && seen_values.insert(HashableExpr::from(*comparator))
126+
});
127+
124128
let mut diagnostic = Diagnostic::new(
125129
RepeatedEqualityComparison {
126130
expression: SourceCodeSnippet::new(merged_membership_test(
127131
expr,
128132
bool_op.op,
129133
&comparators,
130134
checker.locator(),
135+
use_set,
131136
)),
132137
},
133138
bool_op.range(),
@@ -140,6 +145,20 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
140145
let before = bool_op.values.iter().take(*first).cloned();
141146
let after = bool_op.values.iter().skip(last + 1).cloned();
142147

148+
let comparator = if use_set {
149+
Expr::Set(ast::ExprSet {
150+
elts: comparators.iter().copied().cloned().collect(),
151+
range: TextRange::default(),
152+
})
153+
} else {
154+
Expr::Tuple(ast::ExprTuple {
155+
elts: comparators.iter().copied().cloned().collect(),
156+
range: TextRange::default(),
157+
ctx: ExprContext::Load,
158+
parenthesized: true,
159+
})
160+
};
161+
143162
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
144163
checker.generator().expr(&Expr::BoolOp(ast::ExprBoolOp {
145164
op: bool_op.op,
@@ -150,12 +169,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
150169
BoolOp::Or => Box::from([CmpOp::In]),
151170
BoolOp::And => Box::from([CmpOp::NotIn]),
152171
},
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-
})]),
172+
comparators: Box::from([comparator]),
159173
range: bool_op.range(),
160174
})))
161175
.chain(after)
@@ -231,11 +245,13 @@ fn to_allowed_value<'a>(
231245
}
232246

233247
/// Generate a string like `obj in (a, b, c)` or `obj not in (a, b, c)`.
248+
/// If `use_set` is `true`, the string will use a set instead of a tuple.
234249
fn merged_membership_test(
235250
left: &Expr,
236251
op: BoolOp,
237252
comparators: &[&Expr],
238253
locator: &Locator,
254+
use_set: bool,
239255
) -> String {
240256
let op = match op {
241257
BoolOp::Or => "in",
@@ -246,5 +262,10 @@ fn merged_membership_test(
246262
.iter()
247263
.map(|comparator| locator.slice(comparator))
248264
.join(", ");
265+
266+
if use_set {
267+
return format!("{left} {op} {{{members}}}",);
268+
}
269+
249270
format!("{left} {op} ({members})",)
250271
}

0 commit comments

Comments
 (0)