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

New lints: impossible_comparisons and redundant_comparisons #10843

Merged
merged 11 commits into from
Aug 6, 2023

Conversation

MortenLohne
Copy link
Contributor

@MortenLohne MortenLohne commented May 29, 2023

Inspired by a bug we had in production, like all good lints ;)

Adds two lints for "double" comparisons, specifically when the same expression is being compared against two different constants.

impossible_comparisons checks for expressions that can never be true at all. Example:

status_code <= 400 && status_code > 500

Presumably, the programmer intended to write status_code >= 400 && status_code < 500 in this case.

redundant_comparisons checks for expressions where either half has no effect. Example:

status_code <= 400 && status_code < 500

Works with other literal types like floats and strings, and some cases where the constant is more complex than a literal, see the tests for more.

Limitations and/or future work:

  • Doesn't work if the LHS can have side-effects at all, for example by being a function call
  • Only works for exactly two comparison expressions, so x > y && x > z && x < w won't get checked
  • Doesn't check for comparison expressions combined with ||. Very similar logic could be applied there.

changelog: New lints [impossible_comparisons] and [redundant_comparisons]

@rustbot
Copy link
Collaborator

rustbot commented May 29, 2023

r? @Manishearth

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label May 29, 2023
// Ensure that the binary operator is &&
if and_op.node == BinOpKind::And;

let typeck_results = cx.typeck_results();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should filter by the type first to avoid doing expensive const computations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. This was a bit tricky to implement, at least if we want to detect a > b code where a and b are not the same type. I've pushed a commit that adds test cases for this.

Here's the helper function I came up with for implementing your optimization: (Not committed yet):

// Check that our binary operation is of the form `a CMP b && c CMP d`,
// and that the same *type* occurs at least once on both sides of `&&`
fn is_double_comparison<'tcx>(
    typeck: &TypeckResults<'tcx>,
    left_cond: &'tcx Expr<'tcx>,
    right_cond: &'tcx Expr<'tcx>,
) -> bool {
    if_chain! {
        if let ExprKind::Binary(left_cmp_op, left_left_op, left_right_op) = left_cond.kind;
        if let ExprKind::Binary(right_cmp_op, right_left_op, right_right_op) = right_cond.kind;

        if CmpOp::try_from(left_cmp_op.node).is_ok();
        if CmpOp::try_from(right_cmp_op.node).is_ok();

        then {
            let left_types = [typeck.expr_ty(left_left_op), typeck.expr_ty(left_right_op)];
            let right_types = [typeck.expr_ty(right_left_op), typeck.expr_ty(right_right_op)];

            left_types.iter().any(|typ| right_types.contains(typ))
        }
        else {
            false
        }
    }
}

Honestly, I think we need to benchmark this. The new code adds a fair bit of complexity, and it's not obvious to me that it's a performance win at all. Do you know a good way to benchmark a single lint on real-world code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kinda annoying, you can just delete all other lint passes and run it and see. The clippy channel in zulip may know.

(The problem is that rustc will run all lints including disabled ones, as one giant interleaved pass.)

I was going to suggest that we just expr_ty the two parts and check against concrete numeric types but I do realize that still needs the splitting. Maybe it doesn't matter, and those operators are used with numeric types most of the time anyway.

I'm ok merging this with the lint name updated and if you'd like you can explore benchmarking and optimizing it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I could have matched both halves against a list of concrete types, but that might also a bit involved, since the lint also works for string slices, tuples, arrays, basically anything in Constant. I'd like for the lint to keep working on those types, since I think it's easy to makes mistakes with them as well, e.g. the name < "Jennifer" && name > "Shannon" test case.

I wasn't able to get any benchmarking to work, so I settled on a middle solution and implemented the simplest possible optimization: Check that both sides of the root && operator are themselves binary operations. That should filter out most expressions before the expensive const checking stuff.

/// status_code <= 400 && status_code > 500;
/// ```
#[clippy::version = "1.71.0"]
pub IMPOSSIBLE_DOUBLE_CONST_COMPARISONS,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: unsure about the name. opened a thread https://rust-lang.zulipchat.com/#narrow/stream/257328-clippy/topic/lint.20naming.3A.20double.20comparisons

i think IMPOSSIBLE_DOUBLE_COMPARISONS without the CONST is slightly better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from chat

Maybe something like redundant_comparisons + impossible_comparisons

The double/const part seem more like implementation details to me. Also double made me think of f64 initially

we should probably make these work for all numbers and rename the lints

@Manishearth
Copy link
Member

Manishearth commented Jun 10, 2023

cc @Centri3 for additional review

(I'll hopefully rereview soon)

@MortenLohne MortenLohne changed the title New lints: impossible_double_const_comparisons and ineffective_double_const_comparisons New lints: impossible_comparisons and redundant_comparisons Jun 10, 2023
@MortenLohne
Copy link
Contributor Author

I've completed the rename into impossible_comparisons and redundant_comparisons respectively, from discussion on Zulip.

Copy link
Member

@Centri3 Centri3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have the chance to take a closer look currently, but from what I see of the implementation it looks pretty good to me :) I gave a suggestion for the lints' descriptions.

clippy_lints/src/operators/mod.rs Outdated Show resolved Hide resolved
///
/// ### Why is this bad?
/// The whole expression can be replaced by `false`,
/// which is probably not the programmer's intention
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, just add a period to the end of both of these sentences :)

Copy link
Member

@Manishearth Manishearth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r=me after addressing Centri3's comments

Copy link
Member

@Centri3 Centri3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this still looks good to me :) Just some minor nits, thanks!

// Extract a comparison between a const and non-const
// Flip yoda conditionals, turnings expressions like `42 < x` into `x > 42`
fn comparison_to_const<'tcx>(
ctx: &mut ConstEvalLateContext<'_, 'tcx>,
Copy link
Member

@Centri3 Centri3 Jul 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we not use consts::constant instead? It might be marginally slower but ConstEvalLateContext::new does nothing spectacular so doing it twice isn't too impactful. It's at most a micro-optimization. It would just be cleaner.

then {
if left_cmp_op.direction() == right_cmp_op.direction() {
let lhs_str = snippet(cx, left_cond.span, "");
let rhs_str = snippet(cx, right_cond.span, "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We should either use snippet_opt or change "" to something like "<lhs>" instead

else if !comparison_is_possible(left_cmp_op.direction(), ordering) {
let expr_str = snippet(cx, left_expr.span, "");
let lhs_str = snippet(cx, left_const_expr.span, "");
let rhs_str = snippet(cx, right_const_expr.span, "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Ditto

@MortenLohne MortenLohne force-pushed the feat/double-const-comparisons branch from ad5ea9c to 3157b96 Compare August 6, 2023 11:49
@Manishearth
Copy link
Member

@bors r=Centri3

@bors
Copy link
Collaborator

bors commented Aug 6, 2023

📌 Commit d628046 has been approved by Centri3

It is now in the queue for this repository.

@bors
Copy link
Collaborator

bors commented Aug 6, 2023

⌛ Testing commit d628046 with merge 6a2c8a1...

@bors
Copy link
Collaborator

bors commented Aug 6, 2023

☀️ Test successful - checks-action_dev_test, checks-action_remark_test, checks-action_test
Approved by: Centri3
Pushing 6a2c8a1 to master...

@bors bors merged commit 6a2c8a1 into rust-lang:master Aug 6, 2023
7 checks passed
@bors bors mentioned this pull request Aug 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants