Skip to content
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
merged 23 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
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 Sep 21, 2024
583a6d4
fix(pylint): update schema
vincevannoort Sep 21, 2024
cf59b3f
fix(pylint): set as safe edit
vincevannoort Sep 22, 2024
86a599b
fix(pylint): false positives
vincevannoort Sep 22, 2024
fab0996
fix(pylint): update tests
vincevannoort Sep 22, 2024
dce9440
feat(pylint): update `message` and `fix_title`
vincevannoort Sep 22, 2024
faaf682
fix(pylint): update typo
vincevannoort Sep 22, 2024
e596c10
fix(pylint): remove space from example
vincevannoort Sep 22, 2024
7a973a5
fix(pylint): update documentation
vincevannoort Sep 22, 2024
483a305
fix(pylint): convert rule from `stable` to `preview`
vincevannoort Sep 24, 2024
a453653
fix(pylint): remove comment
vincevannoort Sep 24, 2024
cbdfc41
fix(pylint): remove early exit check
vincevannoort Sep 24, 2024
757f6fd
fix(pylint): import range trait
vincevannoort Sep 24, 2024
ffeaf68
fix(pylint): remove collect
vincevannoort Sep 24, 2024
d6eaac6
fix(pylint): use optionals instead of results
vincevannoort Sep 24, 2024
6dfa935
fix(pylint): put values in edit above diagnostic
vincevannoort Sep 24, 2024
a476783
fix(pylint): update `fix_title`
vincevannoort Sep 24, 2024
8dd1ea6
fix(pylint): update tests
vincevannoort Sep 24, 2024
d05191d
fix(pylint): match ops using let pattern
vincevannoort Sep 25, 2024
c9b34c2
Remove unnecessary collects
MichaReiser Sep 25, 2024
4090f6c
Remove fix-only fields from violation
MichaReiser Sep 25, 2024
203e364
Set correct fixability
MichaReiser Sep 25, 2024
fa64950
Remove TODO from tests, use and instead of or in one case
MichaReiser Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Contributor Author

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.

Copy link
Member

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.

pass
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
Expr::BoolOp(bool_op) => {
if checker.enabled(Rule::BooleanChainedComparison) {
pylint::rules::boolean_chained_comparison(checker, bool_op);
}
if checker.enabled(Rule::MultipleStartsEndsWith) {
flake8_pie::rules::multiple_starts_ends_with(checker, expr);
}
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1730") => (RuleGroup::Stable, rules::pylint::rules::IfStmtMinMax),
(Pylint, "R1716") => (RuleGroup::Stable, rules::pylint::rules::BooleanChainedComparison),
vincevannoort marked this conversation as resolved.
Show resolved Hide resolved
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ mod tests {
#[test_case(Rule::BadStringFormatType, Path::new("bad_string_format_type.py"))]
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.py"))]
#[test_case(Rule::BinaryOpException, Path::new("binary_op_exception.py"))]
#[test_case(
Rule::BooleanChainedComparison,
Path::new("boolean_chained_comparison.py")
)]
#[test_case(Rule::CollapsibleElseIf, Path::new("collapsible_else_if.py"))]
#[test_case(Rule::CompareToEmptyString, Path::new("compare_to_empty_string.py"))]
#[test_case(Rule::ComparisonOfConstant, Path::new("comparison_of_constant.py"))]
Expand Down
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
}
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(crate) use bad_string_format_character::BadStringFormatCharacter;
pub(crate) use bad_string_format_type::*;
pub(crate) use bidirectional_unicode::*;
pub(crate) use binary_op_exception::*;
pub(crate) use boolean_chained_comparison::*;
pub(crate) use collapsible_else_if::*;
pub(crate) use compare_to_empty_string::*;
pub(crate) use comparison_of_constant::*;
Expand Down Expand Up @@ -112,6 +113,7 @@ pub(crate) mod bad_string_format_character;
mod bad_string_format_type;
mod bidirectional_unicode;
mod binary_op_exception;
mod boolean_chained_comparison;
mod collapsible_else_if;
mod compare_to_empty_string;
mod comparison_of_constant;
Expand Down
Loading
Loading