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 all 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,120 @@
# ------------------
# 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 and 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

a = int(input())
b = int(input())
c = int(input())
if a > b and b < c:
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::Preview, rules::pylint::rules::BooleanChainedComparison),
(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,120 @@
use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{name::Name, BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare};
use ruff_text_size::{Ranged, 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,
}

impl AlwaysFixableViolation for BooleanChainedComparison {
#[derive_message_formats]
fn message(&self) -> String {
format!("Contains chained boolean comparison that can be simplified")
}

fn fix_title(&self) -> String {
"Use a single compare expression".to_string()
}
}

/// PLR1716
pub(crate) fn boolean_chained_comparison(checker: &mut Checker, expr_bool_op: &ExprBoolOp) {
// early exit for non `and` boolean operations
if expr_bool_op.op != BoolOp::And {
return;
}

// early exit when not all expressions are compare expressions
if !expr_bool_op.values.iter().all(Expr::is_compare_expr) {
return;
}

// retrieve all compare statements from expression
let compare_expressions = expr_bool_op
.values
.iter()
.map(|stmt| stmt.as_compare_expr().unwrap());

let diagnostics = compare_expressions
.tuple_windows()
.filter(|(left_compare, right_compare)| {
are_compare_expr_simplifiable(left_compare, right_compare)
})
.filter_map(|(left_compare, right_compare)| {
let Expr::Name(left_compare_right) = left_compare.comparators.first()? else {
return None;
};

let Expr::Name(right_compare_left) = &*right_compare.left else {
return None;
};

if left_compare_right.id() != right_compare_left.id() {
return None;
}

let edit = Edit::range_replacement(
left_compare_right.id().to_string(),
TextRange::new(left_compare_right.start(), right_compare_left.end()),
);

Some(
Diagnostic::new(
BooleanChainedComparison {
variable: left_compare_right.id().clone(),
},
TextRange::new(left_compare.start(), right_compare.end()),
)
.with_fix(Fix::safe_edit(edit)),
)
});

checker.diagnostics.extend(diagnostics);
}

/// Checks whether two compare expressions are simplifiable
fn are_compare_expr_simplifiable(left: &ExprCompare, right: &ExprCompare) -> bool {
let [left_operator] = &*left.ops else {
return false;
};

let [right_operator] = &*right.ops else {
return false;
};

matches!(
(left_operator, right_operator),
(CmpOp::Lt | CmpOp::LtE, CmpOp::Lt | CmpOp::LtE)
| (CmpOp::Gt | CmpOp::GtE, CmpOp::Gt | CmpOp::GtE)
)
}
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