-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[pylint] Implement boolean-chained-comparison
(R1716
)
#13435
Merged
MichaReiser
merged 23 commits into
astral-sh:main
from
vincevannoort:pylint/chained-comparison
Sep 25, 2024
+513
−0
Merged
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
317a182
feat(pylint): implement `PLR1716`
vincevannoort 583a6d4
fix(pylint): update schema
vincevannoort cf59b3f
fix(pylint): set as safe edit
vincevannoort 86a599b
fix(pylint): false positives
vincevannoort fab0996
fix(pylint): update tests
vincevannoort dce9440
feat(pylint): update `message` and `fix_title`
vincevannoort faaf682
fix(pylint): update typo
vincevannoort e596c10
fix(pylint): remove space from example
vincevannoort 7a973a5
fix(pylint): update documentation
vincevannoort 483a305
fix(pylint): convert rule from `stable` to `preview`
vincevannoort a453653
fix(pylint): remove comment
vincevannoort cbdfc41
fix(pylint): remove early exit check
vincevannoort 757f6fd
fix(pylint): import range trait
vincevannoort ffeaf68
fix(pylint): remove collect
vincevannoort d6eaac6
fix(pylint): use optionals instead of results
vincevannoort 6dfa935
fix(pylint): put values in edit above diagnostic
vincevannoort a476783
fix(pylint): update `fix_title`
vincevannoort 8dd1ea6
fix(pylint): update tests
vincevannoort d05191d
fix(pylint): match ops using let pattern
vincevannoort c9b34c2
Remove unnecessary collects
MichaReiser 4090f6c
Remove fix-only fields from violation
MichaReiser 203e364
Set correct fixability
MichaReiser fa64950
Remove TODO from tests, use and instead of or in one case
MichaReiser File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
crates/ruff_linter/resources/test/fixtures/pylint/boolean_chained_comparison.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# ------------------ | ||
# less than examples | ||
# ------------------ | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a < b and b < c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a < b and b <= c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a <= b and b < c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a <= b and b <= c: # [boolean-chained-comparison] | ||
pass | ||
|
||
# --------------------- | ||
# greater than examples | ||
# --------------------- | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a > b and b > c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a >= b and b > c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a > b and b >= c: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a >= b and b >= c: # [boolean-chained-comparison] | ||
pass | ||
|
||
# ----------------------- | ||
# multiple fixes examples | ||
# ----------------------- | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
d = int(input()) | ||
if a < b and b < c and c < d: # [boolean-chained-comparison] | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
d = int(input()) | ||
e = int(input()) | ||
if a < b and b < c and c < d and d < e: # [boolean-chained-comparison] | ||
pass | ||
|
||
# ------------ | ||
# bad examples | ||
# ------------ | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a > b or b > c: | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a > b or b in (1, 2): | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a < b and b > c: | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a < b and b >= c: | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a <= b and b > c: | ||
pass | ||
|
||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a <= b and b >= c: | ||
pass | ||
|
||
# TODO: check if this example should be fixable or not | ||
a = int(input()) | ||
b = int(input()) | ||
c = int(input()) | ||
if a > b and b < c: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
use itertools::Itertools; | ||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::{name::Name, BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare}; | ||
use ruff_text_size::TextRange; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Check for chained boolean operations that can be simplified. | ||
/// | ||
/// ## Why is this bad? | ||
/// Refactoring the code will improve readability for these cases. | ||
/// | ||
/// ## Example | ||
/// | ||
/// ```python | ||
/// a = int(input()) | ||
/// b = int(input()) | ||
/// c = int(input()) | ||
/// if a < b and b < c: | ||
/// pass | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// | ||
/// ```python | ||
/// a = int(input()) | ||
/// b = int(input()) | ||
/// c = int(input()) | ||
/// if a < b < c: | ||
/// pass | ||
/// ``` | ||
#[violation] | ||
pub struct BooleanChainedComparison { | ||
variable: Name, | ||
range: TextRange, | ||
replace_range: TextRange, | ||
} | ||
|
||
impl Violation for BooleanChainedComparison { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
|
||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Contains chained boolean comparison that can be simplified") | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
Some("Simplify chained boolean comparisons".to_string()) | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/// PLR1716 | ||
pub(crate) fn boolean_chained_comparison(checker: &mut Checker, expr_bool_op: &ExprBoolOp) { | ||
// early out for boolean expression without multiple compare values | ||
if expr_bool_op.values.len() == 1 { | ||
return; | ||
} | ||
|
||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// early out for non `and` boolean operations | ||
if expr_bool_op.op != BoolOp::And { | ||
return; | ||
} | ||
|
||
// early exit when not all expressions are compare expressions | ||
// TODO: check if this can even happen? | ||
if !expr_bool_op.values.iter().all(Expr::is_compare_expr) { | ||
return; | ||
} | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// retrieve all compare statements from expression | ||
let compare_exprs: Vec<&ExprCompare> = expr_bool_op | ||
.values | ||
.iter() | ||
.map(|stmt| stmt.as_compare_expr().unwrap()) | ||
.collect(); | ||
|
||
let results: Vec<Result<(), BooleanChainedComparison>> = compare_exprs | ||
.iter() | ||
.tuple_windows::<(&&ExprCompare, &&ExprCompare)>() | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.filter(|(left_compare, right_compare)| { | ||
are_compare_expr_simplifiable(left_compare, right_compare) | ||
}) | ||
.map(|(left_compare, right_compare)| { | ||
let Expr::Name(left_compare_right) = left_compare.comparators.first().unwrap() else { | ||
return Ok(()); | ||
}; | ||
|
||
let Expr::Name(ref right_compare_left) = right_compare.left.as_ref() else { | ||
return Ok(()); | ||
}; | ||
|
||
if left_compare_right.id() != right_compare_left.id() { | ||
return Ok(()); | ||
} | ||
|
||
Err(BooleanChainedComparison { | ||
variable: left_compare_right.id().clone(), | ||
range: TextRange::new(left_compare.range.start(), right_compare.range.end()), | ||
replace_range: TextRange::new( | ||
left_compare_right.range.start(), | ||
right_compare_left.range.end(), | ||
), | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
}) | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.collect(); | ||
|
||
let results: Vec<BooleanChainedComparison> = results | ||
.into_iter() | ||
.filter(Result::is_err) | ||
.map(|result| result.err().unwrap()) | ||
.collect(); | ||
|
||
checker.diagnostics.extend( | ||
results | ||
.into_iter() | ||
.map(|result| { | ||
let variable = result.variable.clone(); | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let range = result.range; | ||
let replace_range = result.replace_range; | ||
let mut diagnostic = Diagnostic::new(result, range); | ||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( | ||
variable.to_string(), | ||
replace_range, | ||
))); | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
diagnostic | ||
}) | ||
.collect::<Vec<Diagnostic>>(), | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
} | ||
|
||
/// Checks whether two compare expressions are simplifiable | ||
fn are_compare_expr_simplifiable(left: &ExprCompare, right: &ExprCompare) -> bool { | ||
// only allow simplifying simple compare operations | ||
if left.ops.len() != 1 || right.ops.len() != 1 { | ||
return false; | ||
} | ||
|
||
matches!( | ||
(left.ops.first().unwrap(), right.ops.first().unwrap()), | ||
(CmpOp::Lt | CmpOp::LtE, CmpOp::Lt | CmpOp::LtE) | ||
| (CmpOp::Gt | CmpOp::GtE, CmpOp::Gt | CmpOp::GtE) | ||
) | ||
vincevannoort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This to me does seem like a case where it could be written as a single expression
a > b < c
, but I am not entirely sure.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly, but I'm not convinced that it improves readability.