Skip to content
/ rust Public
forked from rust-lang/rust

Commit 6a2c8a1

Browse files
committed
Auto merge of rust-lang#10843 - MortenLohne:feat/double-const-comparisons, r=Centri3
New lints: `impossible_comparisons` and `redundant_comparisons` 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: ```rust 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: ```rust 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`]
2 parents 5818225 + d628046 commit 6a2c8a1

10 files changed

+601
-22
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4897,6 +4897,7 @@ Released 2018-09-13
48974897
[`implicit_return`]: https://rust-lang.github.io/rust-clippy/master/index.html#implicit_return
48984898
[`implicit_saturating_add`]: https://rust-lang.github.io/rust-clippy/master/index.html#implicit_saturating_add
48994899
[`implicit_saturating_sub`]: https://rust-lang.github.io/rust-clippy/master/index.html#implicit_saturating_sub
4900+
[`impossible_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#impossible_comparisons
49004901
[`imprecise_flops`]: https://rust-lang.github.io/rust-clippy/master/index.html#imprecise_flops
49014902
[`inconsistent_digit_grouping`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_digit_grouping
49024903
[`inconsistent_struct_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_struct_constructor
@@ -5191,6 +5192,7 @@ Released 2018-09-13
51915192
[`redundant_closure`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure
51925193
[`redundant_closure_call`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_call
51935194
[`redundant_closure_for_method_calls`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_for_method_calls
5195+
[`redundant_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_comparisons
51945196
[`redundant_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_else
51955197
[`redundant_feature_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_feature_names
51965198
[`redundant_field_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_field_names

clippy_lints/src/declared_lints.rs

+2
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
518518
crate::operators::FLOAT_CMP_CONST_INFO,
519519
crate::operators::FLOAT_EQUALITY_WITHOUT_ABS_INFO,
520520
crate::operators::IDENTITY_OP_INFO,
521+
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
521522
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
522523
crate::operators::INTEGER_DIVISION_INFO,
523524
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
@@ -526,6 +527,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
526527
crate::operators::NEEDLESS_BITWISE_BOOL_INFO,
527528
crate::operators::OP_REF_INFO,
528529
crate::operators::PTR_EQ_INFO,
530+
crate::operators::REDUNDANT_COMPARISONS_INFO,
529531
crate::operators::SELF_ASSIGNMENT_INFO,
530532
crate::operators::VERBOSE_BIT_MASK_INFO,
531533
crate::option_env_unwrap::OPTION_ENV_UNWRAP_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#![allow(clippy::match_same_arms)]
2+
3+
use std::cmp::Ordering;
4+
5+
use clippy_utils::consts::{constant, Constant};
6+
use if_chain::if_chain;
7+
use rustc_hir::{BinOpKind, Expr, ExprKind};
8+
use rustc_lint::LateContext;
9+
use rustc_middle::ty::layout::HasTyCtxt;
10+
use rustc_middle::ty::{Ty, TypeckResults};
11+
use rustc_span::source_map::{Span, Spanned};
12+
13+
use clippy_utils::diagnostics::span_lint_and_note;
14+
use clippy_utils::source::snippet;
15+
use clippy_utils::SpanlessEq;
16+
17+
use super::{IMPOSSIBLE_COMPARISONS, REDUNDANT_COMPARISONS};
18+
19+
// Extract a comparison between a const and non-const
20+
// Flip yoda conditionals, turnings expressions like `42 < x` into `x > 42`
21+
fn comparison_to_const<'tcx>(
22+
cx: &LateContext<'tcx>,
23+
typeck: &TypeckResults<'tcx>,
24+
expr: &'tcx Expr<'tcx>,
25+
) -> Option<(CmpOp, &'tcx Expr<'tcx>, &'tcx Expr<'tcx>, Constant<'tcx>, Ty<'tcx>)> {
26+
if_chain! {
27+
if let ExprKind::Binary(operator, left, right) = expr.kind;
28+
if let Ok(cmp_op) = CmpOp::try_from(operator.node);
29+
then {
30+
match (constant(cx, typeck, left), constant(cx, typeck, right)) {
31+
(Some(_), Some(_)) => None,
32+
(_, Some(con)) => Some((cmp_op, left, right, con, typeck.expr_ty(right))),
33+
(Some(con), _) => Some((cmp_op.reverse(), right, left, con, typeck.expr_ty(left))),
34+
_ => None,
35+
}
36+
} else {
37+
None
38+
}
39+
}
40+
}
41+
42+
pub(super) fn check<'tcx>(
43+
cx: &LateContext<'tcx>,
44+
and_op: Spanned<BinOpKind>,
45+
left_cond: &'tcx Expr<'tcx>,
46+
right_cond: &'tcx Expr<'tcx>,
47+
span: Span,
48+
) {
49+
if_chain! {
50+
// Ensure that the binary operator is &&
51+
if and_op.node == BinOpKind::And;
52+
53+
// Check that both operands to '&&' are themselves a binary operation
54+
// The `comparison_to_const` step also checks this, so this step is just an optimization
55+
if let ExprKind::Binary(_, _, _) = left_cond.kind;
56+
if let ExprKind::Binary(_, _, _) = right_cond.kind;
57+
58+
let typeck = cx.typeck_results();
59+
60+
// Check that both operands to '&&' compare a non-literal to a literal
61+
if let Some((left_cmp_op, left_expr, left_const_expr, left_const, left_type)) =
62+
comparison_to_const(cx, typeck, left_cond);
63+
if let Some((right_cmp_op, right_expr, right_const_expr, right_const, right_type)) =
64+
comparison_to_const(cx, typeck, right_cond);
65+
66+
if left_type == right_type;
67+
68+
// Check that the same expression is compared in both comparisons
69+
if SpanlessEq::new(cx).eq_expr(left_expr, right_expr);
70+
71+
if !left_expr.can_have_side_effects();
72+
73+
// Compare the two constant expressions
74+
if let Some(ordering) = Constant::partial_cmp(cx.tcx(), left_type, &left_const, &right_const);
75+
76+
// Rule out the `x >= 42 && x <= 42` corner case immediately
77+
// Mostly to simplify the implementation, but it is also covered by `clippy::double_comparisons`
78+
if !matches!(
79+
(&left_cmp_op, &right_cmp_op, ordering),
80+
(CmpOp::Le | CmpOp::Ge, CmpOp::Le | CmpOp::Ge, Ordering::Equal)
81+
);
82+
83+
then {
84+
if left_cmp_op.direction() == right_cmp_op.direction() {
85+
let lhs_str = snippet(cx, left_cond.span, "<lhs>");
86+
let rhs_str = snippet(cx, right_cond.span, "<rhs>");
87+
// We already know that either side of `&&` has no effect,
88+
// but emit a different error message depending on which side it is
89+
if left_side_is_useless(left_cmp_op, ordering) {
90+
span_lint_and_note(
91+
cx,
92+
REDUNDANT_COMPARISONS,
93+
span,
94+
"left-hand side of `&&` operator has no effect",
95+
Some(left_cond.span.until(right_cond.span)),
96+
&format!("`if `{rhs_str}` evaluates to true, {lhs_str}` will always evaluate to true as well"),
97+
);
98+
} else {
99+
span_lint_and_note(
100+
cx,
101+
REDUNDANT_COMPARISONS,
102+
span,
103+
"right-hand side of `&&` operator has no effect",
104+
Some(and_op.span.to(right_cond.span)),
105+
&format!("`if `{lhs_str}` evaluates to true, {rhs_str}` will always evaluate to true as well"),
106+
);
107+
}
108+
// We could autofix this error but choose not to,
109+
// because code triggering this lint probably not behaving correctly in the first place
110+
}
111+
else if !comparison_is_possible(left_cmp_op.direction(), ordering) {
112+
let expr_str = snippet(cx, left_expr.span, "..");
113+
let lhs_str = snippet(cx, left_const_expr.span, "<lhs>");
114+
let rhs_str = snippet(cx, right_const_expr.span, "<rhs>");
115+
let note = match ordering {
116+
Ordering::Less => format!("since `{lhs_str}` < `{rhs_str}`, the expression evaluates to false for any value of `{expr_str}`"),
117+
Ordering::Equal => format!("`{expr_str}` cannot simultaneously be greater than and less than `{lhs_str}`"),
118+
Ordering::Greater => format!("since `{lhs_str}` > `{rhs_str}`, the expression evaluates to false for any value of `{expr_str}`"),
119+
};
120+
span_lint_and_note(
121+
cx,
122+
IMPOSSIBLE_COMPARISONS,
123+
span,
124+
"boolean expression will never evaluate to 'true'",
125+
None,
126+
&note,
127+
);
128+
};
129+
}
130+
}
131+
}
132+
133+
fn left_side_is_useless(left_cmp_op: CmpOp, ordering: Ordering) -> bool {
134+
// Special-case for equal constants with an inclusive comparison
135+
if ordering == Ordering::Equal {
136+
match left_cmp_op {
137+
CmpOp::Lt | CmpOp::Gt => false,
138+
CmpOp::Le | CmpOp::Ge => true,
139+
}
140+
} else {
141+
match (left_cmp_op.direction(), ordering) {
142+
(CmpOpDirection::Lesser, Ordering::Less) => false,
143+
(CmpOpDirection::Lesser, Ordering::Equal) => false,
144+
(CmpOpDirection::Lesser, Ordering::Greater) => true,
145+
(CmpOpDirection::Greater, Ordering::Less) => true,
146+
(CmpOpDirection::Greater, Ordering::Equal) => false,
147+
(CmpOpDirection::Greater, Ordering::Greater) => false,
148+
}
149+
}
150+
}
151+
152+
fn comparison_is_possible(left_cmp_direction: CmpOpDirection, ordering: Ordering) -> bool {
153+
match (left_cmp_direction, ordering) {
154+
(CmpOpDirection::Lesser, Ordering::Less | Ordering::Equal) => false,
155+
(CmpOpDirection::Lesser, Ordering::Greater) => true,
156+
(CmpOpDirection::Greater, Ordering::Greater | Ordering::Equal) => false,
157+
(CmpOpDirection::Greater, Ordering::Less) => true,
158+
}
159+
}
160+
161+
#[derive(PartialEq, Eq, Clone, Copy)]
162+
enum CmpOpDirection {
163+
Lesser,
164+
Greater,
165+
}
166+
167+
#[derive(Clone, Copy)]
168+
enum CmpOp {
169+
Lt,
170+
Le,
171+
Ge,
172+
Gt,
173+
}
174+
175+
impl CmpOp {
176+
fn reverse(self) -> Self {
177+
match self {
178+
CmpOp::Lt => CmpOp::Gt,
179+
CmpOp::Le => CmpOp::Ge,
180+
CmpOp::Ge => CmpOp::Le,
181+
CmpOp::Gt => CmpOp::Lt,
182+
}
183+
}
184+
185+
fn direction(self) -> CmpOpDirection {
186+
match self {
187+
CmpOp::Lt => CmpOpDirection::Lesser,
188+
CmpOp::Le => CmpOpDirection::Lesser,
189+
CmpOp::Ge => CmpOpDirection::Greater,
190+
CmpOp::Gt => CmpOpDirection::Greater,
191+
}
192+
}
193+
}
194+
195+
impl TryFrom<BinOpKind> for CmpOp {
196+
type Error = ();
197+
198+
fn try_from(bin_op: BinOpKind) -> Result<Self, Self::Error> {
199+
match bin_op {
200+
BinOpKind::Lt => Ok(CmpOp::Lt),
201+
BinOpKind::Le => Ok(CmpOp::Le),
202+
BinOpKind::Ge => Ok(CmpOp::Ge),
203+
BinOpKind::Gt => Ok(CmpOp::Gt),
204+
_ => Err(()),
205+
}
206+
}
207+
}

clippy_lints/src/operators/mod.rs

+43
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod absurd_extreme_comparisons;
22
mod assign_op_pattern;
33
mod bit_mask;
44
mod cmp_owned;
5+
mod const_comparisons;
56
mod double_comparison;
67
mod duration_subsec;
78
mod eq_op;
@@ -298,6 +299,45 @@ declare_clippy_lint! {
298299
"unnecessary double comparisons that can be simplified"
299300
}
300301

302+
declare_clippy_lint! {
303+
/// ### What it does
304+
/// Checks for double comparisons that can never succeed
305+
///
306+
/// ### Why is this bad?
307+
/// The whole expression can be replaced by `false`,
308+
/// which is probably not the programmer's intention
309+
///
310+
/// ### Example
311+
/// ```rust
312+
/// # let status_code = 200;
313+
/// if status_code <= 400 && status_code > 500 {}
314+
/// ```
315+
#[clippy::version = "1.71.0"]
316+
pub IMPOSSIBLE_COMPARISONS,
317+
correctness,
318+
"double comparisons that will never evaluate to `true`"
319+
}
320+
321+
declare_clippy_lint! {
322+
/// ### What it does
323+
/// Checks for ineffective double comparisons against constants.
324+
///
325+
/// ### Why is this bad?
326+
/// Only one of the comparisons has any effect on the result, the programmer
327+
/// probably intended to flip one of the comparison operators, or compare a
328+
/// different value entirely.
329+
///
330+
/// ### Example
331+
/// ```rust
332+
/// # let status_code = 200;
333+
/// if status_code <= 400 && status_code < 500 {}
334+
/// ```
335+
#[clippy::version = "1.71.0"]
336+
pub REDUNDANT_COMPARISONS,
337+
correctness,
338+
"double comparisons where one of them can be removed"
339+
}
340+
301341
declare_clippy_lint! {
302342
/// ### What it does
303343
/// Checks for calculation of subsecond microseconds or milliseconds
@@ -742,6 +782,8 @@ impl_lint_pass!(Operators => [
742782
INEFFECTIVE_BIT_MASK,
743783
VERBOSE_BIT_MASK,
744784
DOUBLE_COMPARISONS,
785+
IMPOSSIBLE_COMPARISONS,
786+
REDUNDANT_COMPARISONS,
745787
DURATION_SUBSEC,
746788
EQ_OP,
747789
OP_REF,
@@ -786,6 +828,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
786828
bit_mask::check(cx, e, op.node, lhs, rhs);
787829
verbose_bit_mask::check(cx, e, op.node, lhs, rhs, self.verbose_bit_mask_threshold);
788830
double_comparison::check(cx, op.node, lhs, rhs, e.span);
831+
const_comparisons::check(cx, op, lhs, rhs, e.span);
789832
duration_subsec::check(cx, e, op.node, lhs, rhs);
790833
float_equality_without_abs::check(cx, e, op.node, lhs, rhs);
791834
integer_division::check(cx, e, op.node, lhs, rhs);

clippy_utils/src/consts.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ pub struct ConstEvalLateContext<'a, 'tcx> {
329329
}
330330

331331
impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
332-
fn new(lcx: &'a LateContext<'tcx>, typeck_results: &'a ty::TypeckResults<'tcx>) -> Self {
332+
pub fn new(lcx: &'a LateContext<'tcx>, typeck_results: &'a ty::TypeckResults<'tcx>) -> Self {
333333
Self {
334334
lcx,
335335
typeck_results,

0 commit comments

Comments
 (0)