-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
New lints: impossible_comparisons
and redundant_comparisons
#10843
Conversation
r? @Manishearth (rustbot has picked a reviewer for you, use r? to override) |
// Ensure that the binary operator is && | ||
if and_op.node == BinOpKind::And; | ||
|
||
let typeck_results = cx.typeck_results(); |
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.
nit: we should filter by the type first to avoid doing expensive const computations
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.
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?
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.
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.
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.
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.
clippy_lints/src/operators/mod.rs
Outdated
/// status_code <= 400 && status_code > 500; | ||
/// ``` | ||
#[clippy::version = "1.71.0"] | ||
pub IMPOSSIBLE_DOUBLE_CONST_COMPARISONS, |
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.
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
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.
from chat
Maybe something like
redundant_comparisons
+impossible_comparisons
The
double
/const
part seem more like implementation details to me. Alsodouble
made me think off64
initially
we should probably make these work for all numbers and rename the lints
cc @Centri3 for additional review (I'll hopefully rereview soon) |
impossible_double_const_comparisons
and ineffective_double_const_comparisons
impossible_comparisons
and redundant_comparisons
I've completed the rename into |
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.
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.
/// | ||
/// ### Why is this bad? | ||
/// The whole expression can be replaced by `false`, | ||
/// which is probably not the programmer's intention |
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.
Here, just add a period to the end of both of these sentences :)
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.
r=me after addressing Centri3's comments
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.
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>, |
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.
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, ""); |
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.
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, ""); |
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.
nit: Ditto
…context()' function that it replaced
…ble_const_comparisons`
…ective_double_const_comparisons'
…ns' and 'ineffective_double_const_comparisons' -> 'redundant_comparisons', after discussion on Zulip
…implify the code, after PR suggestion
ad5ea9c
to
3157b96
Compare
Co-authored-by: Catherine Flores <[email protected]>
@bors r=Centri3 |
☀️ Test successful - checks-action_dev_test, checks-action_remark_test, checks-action_test |
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: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: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:
x > y && x > z && x < w
won't get checked||
. Very similar logic could be applied there.changelog: New lints [
impossible_comparisons
] and [redundant_comparisons
]