Preserve unmatched comparators in `SIM109` (#1998)

Closes #1993.
This commit is contained in:
Charlie Marsh 2023-01-19 10:23:20 -05:00 committed by GitHub
parent 6ddfe50ac4
commit a122d95ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 63 deletions

View File

@ -1009,7 +1009,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
| SIM105 | UseContextlibSuppress | Use `contextlib.suppress(...)` instead of try-except-pass | | | SIM105 | UseContextlibSuppress | Use `contextlib.suppress(...)` instead of try-except-pass | |
| SIM107 | ReturnInTryExceptFinally | Don't use `return` in `try`/`except` and `finally` | | | SIM107 | ReturnInTryExceptFinally | Don't use `return` in `try`/`except` and `finally` | |
| SIM108 | UseTernaryOperator | Use ternary operator `...` instead of if-else-block | 🛠 | | SIM108 | UseTernaryOperator | Use ternary operator `...` instead of if-else-block | 🛠 |
| SIM109 | CompareWithTuple | Use `value in (..., ...)` instead of `value == ... or value == ...` | 🛠 | | SIM109 | CompareWithTuple | Use `value in (... ,...)` instead of multiple equality comparisons | 🛠 |
| SIM110 | ConvertLoopToAny | Use `return any(x for x in y)` instead of `for` loop | 🛠 | | SIM110 | ConvertLoopToAny | Use `return any(x for x in y)` instead of `for` loop | 🛠 |
| SIM111 | ConvertLoopToAll | Use `return all(x for x in y)` instead of `for` loop | 🛠 | | SIM111 | ConvertLoopToAll | Use `return all(x for x in y)` instead of `for` loop | 🛠 |
| SIM112 | UseCapitalEnvironmentVariables | Use capitalized environment variable `...` instead of `...` | 🛠 | | SIM112 | UseCapitalEnvironmentVariables | Use capitalized environment variable `...` instead of `...` | 🛠 |

View File

@ -1,7 +1,31 @@
# Bad # SIM109
if a == b or a == c: if a == b or a == c:
d d
# Good # SIM109
if (a == b or a == c) and None:
d
# SIM109
if a == b or a == c or None:
d
# SIM109
if a == b or None or a == c:
d
# OK
if a in (b, c): if a in (b, c):
d d
# OK
if a == b or a == c():
d
# OK
if (
a == b
# This comment prevents us from raising SIM109
or a == c
):
d

View File

@ -2,10 +2,9 @@ use std::collections::BTreeMap;
use std::iter; use std::iter;
use itertools::Either::{Left, Right}; use itertools::Either::{Left, Right};
use rustc_hash::FxHashMap;
use rustpython_ast::{Boolop, Cmpop, Constant, Expr, ExprContext, ExprKind, Unaryop}; use rustpython_ast::{Boolop, Cmpop, Constant, Expr, ExprContext, ExprKind, Unaryop};
use crate::ast::helpers::{contains_effect, create_expr, unparse_expr}; use crate::ast::helpers::{contains_effect, create_expr, has_comments_in, unparse_expr};
use crate::ast::types::Range; use crate::ast::types::Range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::Fix; use crate::fix::Fix;
@ -30,7 +29,7 @@ pub fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
// Locate duplicate `isinstance` calls, represented as a map from argument name // Locate duplicate `isinstance` calls, represented as a map from argument name
// to indices of the relevant `Expr` instances in `values`. // to indices of the relevant `Expr` instances in `values`.
let mut duplicates = FxHashMap::default(); let mut duplicates = BTreeMap::new();
for (index, call) in values.iter().enumerate() { for (index, call) in values.iter().enumerate() {
// Verify that this is an `isinstance` call. // Verify that this is an `isinstance` call.
let ExprKind::Call { func, args, keywords } = &call.node else { let ExprKind::Call { func, args, keywords } = &call.node else {
@ -136,62 +135,98 @@ pub fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
} }
} }
fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> {
let ExprKind::Compare { left, ops, comparators } = &expr.node else {
return None;
};
if ops.len() != 1 || comparators.len() != 1 {
return None;
}
if !matches!(&ops[0], Cmpop::Eq) {
return None;
}
let ExprKind::Name { id, .. } = &left.node else {
return None;
};
let comparator = &comparators[0];
if !matches!(&comparator.node, ExprKind::Name { .. }) {
return None;
}
Some((id, comparator))
}
/// SIM109 /// SIM109
pub fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { pub fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
let ExprKind::BoolOp { op: Boolop::Or, values } = &expr.node else { let ExprKind::BoolOp { op: Boolop::Or, values } = &expr.node else {
return; return;
}; };
let mut id_to_values = BTreeMap::<&str, Vec<&Expr>>::new(); // Given `a == "foo" or a == "bar"`, we generate `{"a": [(0, "foo"), (1,
for value in values { // "bar")]}`.
let ExprKind::Compare { left, ops, comparators } = &value.node else { let mut id_to_comparators: BTreeMap<&str, Vec<(usize, &Expr)>> = BTreeMap::new();
continue; for (index, value) in values.iter().enumerate() {
}; if let Some((id, comparator)) = match_eq_target(value) {
if ops.len() != 1 || comparators.len() != 1 { id_to_comparators
continue; .entry(id)
.or_default()
.push((index, comparator));
} }
if !matches!(&ops[0], Cmpop::Eq) {
continue;
}
let ExprKind::Name { id, .. } = &left.node else {
continue;
};
let comparator = &comparators[0];
if !matches!(&comparator.node, ExprKind::Name { .. }) {
continue;
}
id_to_values.entry(id).or_default().push(comparator);
} }
for (value, values) in id_to_values { for (id, matches) in id_to_comparators {
if values.len() == 1 { if matches.len() == 1 {
continue; continue;
} }
let str_values = values
let (indices, comparators): (Vec<_>, Vec<_>) = matches.iter().copied().unzip();
// Avoid rewriting (e.g.) `a == "foo" or a == f()`.
if comparators
.iter() .iter()
.map(|value| unparse_expr(value, checker.stylist)) .any(|expr| contains_effect(checker, expr))
.collect(); {
let mut diagnostic = Diagnostic::new( continue;
violations::CompareWithTuple( }
value.to_string(),
str_values, // Avoid removing comments.
unparse_expr(expr, checker.stylist), if has_comments_in(Range::from_located(expr), checker.locator) {
), continue;
Range::from_located(expr), }
);
if checker.patch(&Rule::CompareWithTuple) { // Create a `x in (a, b)` expression.
// Create a `x in (a, b)` compare expr.
let in_expr = create_expr(ExprKind::Compare { let in_expr = create_expr(ExprKind::Compare {
left: Box::new(create_expr(ExprKind::Name { left: Box::new(create_expr(ExprKind::Name {
id: value.to_string(), id: id.to_string(),
ctx: ExprContext::Load, ctx: ExprContext::Load,
})), })),
ops: vec![Cmpop::In], ops: vec![Cmpop::In],
comparators: vec![create_expr(ExprKind::Tuple { comparators: vec![create_expr(ExprKind::Tuple {
elts: values.into_iter().map(Clone::clone).collect(), elts: comparators.into_iter().map(Clone::clone).collect(),
ctx: ExprContext::Load, ctx: ExprContext::Load,
})], })],
}); });
let mut diagnostic = Diagnostic::new(
violations::CompareWithTuple {
replacement: unparse_expr(&in_expr, checker.stylist),
},
Range::from_located(expr),
);
if checker.patch(&Rule::CompareWithTuple) {
let unmatched: Vec<Expr> = values
.iter()
.enumerate()
.filter(|(index, _)| !indices.contains(index))
.map(|(_, elt)| elt.clone())
.collect();
let in_expr = if unmatched.is_empty() {
in_expr
} else {
// Wrap in a `x in (a, b) or ...` boolean operation.
create_expr(ExprKind::BoolOp {
op: Boolop::Or,
values: iter::once(in_expr).chain(unmatched).collect(),
})
};
diagnostic.amend(Fix::replacement( diagnostic.amend(Fix::replacement(
unparse_expr(&in_expr, checker.stylist), unparse_expr(&in_expr, checker.stylist),
expr.location, expr.location,

View File

@ -4,10 +4,7 @@ expression: diagnostics
--- ---
- kind: - kind:
CompareWithTuple: CompareWithTuple:
- a replacement: "a in (b, c)"
- - b
- c
- a == b or a == c
location: location:
row: 2 row: 2
column: 3 column: 3
@ -23,4 +20,58 @@ expression: diagnostics
row: 2 row: 2
column: 19 column: 19
parent: ~ parent: ~
- kind:
CompareWithTuple:
replacement: "a in (b, c)"
location:
row: 6
column: 4
end_location:
row: 6
column: 20
fix:
content: "a in (b, c)"
location:
row: 6
column: 4
end_location:
row: 6
column: 20
parent: ~
- kind:
CompareWithTuple:
replacement: "a in (b, c)"
location:
row: 10
column: 3
end_location:
row: 10
column: 27
fix:
content: "a in (b, c) or None"
location:
row: 10
column: 3
end_location:
row: 10
column: 27
parent: ~
- kind:
CompareWithTuple:
replacement: "a in (b, c)"
location:
row: 14
column: 3
end_location:
row: 14
column: 27
fix:
content: "a in (b, c) or None"
location:
row: 14
column: 3
end_location:
row: 14
column: 27
parent: ~

View File

@ -2894,27 +2894,25 @@ impl AlwaysAutofixableViolation for UseTernaryOperator {
} }
define_violation!( define_violation!(
pub struct CompareWithTuple(pub String, pub Vec<String>, pub String); pub struct CompareWithTuple {
pub replacement: String,
}
); );
impl AlwaysAutofixableViolation for CompareWithTuple { impl AlwaysAutofixableViolation for CompareWithTuple {
fn message(&self) -> String { fn message(&self) -> String {
let CompareWithTuple(value, values, or_op) = self; let CompareWithTuple { replacement } = self;
let values = values.join(", "); format!("Use `{replacement}` instead of multiple equality comparisons")
format!("Use `{value} in ({values})` instead of `{or_op}`")
} }
fn autofix_title(&self) -> String { fn autofix_title(&self) -> String {
let CompareWithTuple(value, values, or_op) = self; let CompareWithTuple { replacement, .. } = self;
let values = values.join(", "); format!("Replace with `{replacement}`")
format!("Replace `{or_op}` with `{value} in {values}`")
} }
fn placeholder() -> Self { fn placeholder() -> Self {
CompareWithTuple( CompareWithTuple {
"value".to_string(), replacement: "value in (... ,...)".to_string(),
vec!["...".to_string(), "...".to_string()], }
"value == ... or value == ...".to_string(),
)
} }
} }