Skip to content

Commit 046d3df

Browse files
committed
New lints: impossible_double_const_comparisons and ineffective_double_const_comparisons
1 parent ab1281f commit 046d3df

File tree

9 files changed

+506
-21
lines changed

9 files changed

+506
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
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_double_const_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#impossible_double_const_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
@@ -4905,6 +4906,7 @@ Released 2018-09-13
49054906
[`index_refutable_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#index_refutable_slice
49064907
[`indexing_slicing`]: https://rust-lang.github.io/rust-clippy/master/index.html#indexing_slicing
49074908
[`ineffective_bit_mask`]: https://rust-lang.github.io/rust-clippy/master/index.html#ineffective_bit_mask
4909+
[`ineffective_double_const_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#ineffective_double_const_comparisons
49084910
[`inefficient_to_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#inefficient_to_string
49094911
[`infallible_destructuring_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#infallible_destructuring_match
49104912
[`infinite_iter`]: https://rust-lang.github.io/rust-clippy/master/index.html#infinite_iter

clippy_lints/src/declared_lints.rs

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

clippy_lints/src/operators/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod assign_op_pattern;
33
mod bit_mask;
44
mod cmp_owned;
55
mod double_comparison;
6+
mod double_const_comparison;
67
mod duration_subsec;
78
mod eq_op;
89
mod erasing_op;
@@ -298,6 +299,43 @@ 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+
/// status_code <= 400 && status_code > 500;
313+
/// ```
314+
#[clippy::version = "1.71.0"]
315+
pub IMPOSSIBLE_DOUBLE_CONST_COMPARISONS,
316+
correctness,
317+
"default lint description"
318+
}
319+
320+
declare_clippy_lint! {
321+
/// ### What it does
322+
/// Checks for ineffective double comparisons against constants
323+
///
324+
/// ### Why is this bad?
325+
/// Only one of the comparisons has any effect on the result
326+
/// The programmer probably intended to flip one of the comparison operators,
327+
/// or compare a different value entirely
328+
///
329+
/// ### Example
330+
/// ```rust
331+
/// status_code <= 400 && status_code < 500;
332+
/// ```
333+
#[clippy::version = "1.71.0"]
334+
pub INEFFECTIVE_DOUBLE_CONST_COMPARISONS,
335+
correctness,
336+
"default lint description"
337+
}
338+
301339
declare_clippy_lint! {
302340
/// ### What it does
303341
/// Checks for calculation of subsecond microseconds or milliseconds
@@ -742,6 +780,8 @@ impl_lint_pass!(Operators => [
742780
INEFFECTIVE_BIT_MASK,
743781
VERBOSE_BIT_MASK,
744782
DOUBLE_COMPARISONS,
783+
IMPOSSIBLE_DOUBLE_CONST_COMPARISONS,
784+
INEFFECTIVE_DOUBLE_CONST_COMPARISONS,
745785
DURATION_SUBSEC,
746786
EQ_OP,
747787
OP_REF,
@@ -786,6 +826,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
786826
bit_mask::check(cx, e, op.node, lhs, rhs);
787827
verbose_bit_mask::check(cx, e, op.node, lhs, rhs, self.verbose_bit_mask_threshold);
788828
double_comparison::check(cx, op.node, lhs, rhs, e.span);
829+
double_const_comparison::check(cx, op, lhs, rhs, e.span);
789830
duration_subsec::check(cx, e, op.node, lhs, rhs);
790831
float_equality_without_abs::check(cx, e, op.node, lhs, rhs);
791832
integer_division::check(cx, e, op.node, lhs, rhs);

tests/ui/double_const_comparisons.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![allow(unused)]
2+
#![warn(clippy::impossible_double_const_comparisons)]
3+
#![warn(clippy::ineffective_double_const_comparisons)]
4+
#![allow(clippy::no_effect)]
5+
#![allow(clippy::short_circuit_statement)]
6+
#![allow(clippy::manual_range_contains)]
7+
8+
const STATUS_BAD_REQUEST: u16 = 400;
9+
const STATUS_SERVER_ERROR: u16 = 500;
10+
11+
fn main() {
12+
let status_code = 500; // Value doesn't matter for the lint
13+
14+
status_code >= 400 && status_code < 500; // Correct
15+
status_code <= 400 && status_code > 500;
16+
status_code > 500 && status_code < 400;
17+
status_code < 500 && status_code > 500;
18+
19+
// More complex expressions
20+
status_code < { 400 } && status_code > { 500 };
21+
status_code < STATUS_BAD_REQUEST && status_code > STATUS_SERVER_ERROR;
22+
status_code <= u16::MIN + 1 && status_code > STATUS_SERVER_ERROR;
23+
status_code < STATUS_SERVER_ERROR && status_code > STATUS_SERVER_ERROR;
24+
25+
// Yoda conditions
26+
500 <= status_code && 600 > status_code; // Correct
27+
500 <= status_code && status_code <= 600; // Correct
28+
500 >= status_code && 600 < status_code; // Incorrect
29+
500 >= status_code && status_code > 600; // Incorrect
30+
31+
// Expressions where one of the sides has no effect
32+
status_code < 200 && status_code <= 299;
33+
status_code > 200 && status_code >= 299;
34+
35+
status_code >= 500 && status_code > 500; // Useless left
36+
status_code > 500 && status_code >= 500; // Useless right
37+
status_code <= 500 && status_code < 500; // Useless left
38+
status_code < 500 && status_code <= 500; // Useless right
39+
40+
// Other types
41+
let name = "Steve";
42+
name < "Jennifer" && name > "Shannon";
43+
44+
let numbers = [1, 2];
45+
numbers < [3, 4] && numbers > [5, 6];
46+
47+
let letter = 'a';
48+
letter < 'b' && letter > 'c';
49+
50+
let area = 42.0;
51+
area < std::f32::consts::E && area > std::f32::consts::PI;
52+
}

0 commit comments

Comments
 (0)