Skip to content

Commit c9d2961

Browse files
committed
teach eager_or_lazy that arithmetic ops can panic
1 parent 902c79c commit c9d2961

File tree

5 files changed

+336
-66
lines changed

5 files changed

+336
-66
lines changed

clippy_utils/src/consts.rs

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use rustc_hir::{BinOp, BinOpKind, Block, ConstBlock, Expr, ExprKind, HirId, Item
1010
use rustc_lexer::tokenize;
1111
use rustc_lint::LateContext;
1212
use rustc_middle::mir::interpret::{alloc_range, Scalar};
13-
use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, List, ScalarInt, Ty, TyCtxt};
13+
use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, IntTy, List, ScalarInt, Ty, TyCtxt, UintTy};
1414
use rustc_middle::{bug, mir, span_bug};
1515
use rustc_span::symbol::{Ident, Symbol};
1616
use rustc_span::SyntaxContext;
@@ -51,6 +51,63 @@ pub enum Constant<'tcx> {
5151
Err,
5252
}
5353

54+
trait IntTypeBounds: Sized {
55+
type Output: PartialOrd;
56+
57+
fn min_max(self) -> Option<(Self::Output, Self::Output)>;
58+
fn bits(self) -> Self::Output;
59+
fn ensure_fits(self, val: Self::Output) -> Option<Self::Output> {
60+
let (min, max) = self.min_max()?;
61+
(min <= val && val <= max).then_some(val)
62+
}
63+
}
64+
impl IntTypeBounds for UintTy {
65+
type Output = u128;
66+
fn min_max(self) -> Option<(Self::Output, Self::Output)> {
67+
Some(match self {
68+
UintTy::U8 => (u8::MIN.into(), u8::MAX.into()),
69+
UintTy::U16 => (u16::MIN.into(), u16::MAX.into()),
70+
UintTy::U32 => (u32::MIN.into(), u32::MAX.into()),
71+
UintTy::U64 => (u64::MIN.into(), u64::MAX.into()),
72+
UintTy::U128 => (u128::MIN, u128::MAX),
73+
UintTy::Usize => (usize::MIN.try_into().ok()?, usize::MAX.try_into().ok()?),
74+
})
75+
}
76+
fn bits(self) -> Self::Output {
77+
match self {
78+
UintTy::U8 => 8,
79+
UintTy::U16 => 16,
80+
UintTy::U32 => 32,
81+
UintTy::U64 => 64,
82+
UintTy::U128 => 128,
83+
UintTy::Usize => usize::BITS.into(),
84+
}
85+
}
86+
}
87+
impl IntTypeBounds for IntTy {
88+
type Output = i128;
89+
fn min_max(self) -> Option<(Self::Output, Self::Output)> {
90+
Some(match self {
91+
IntTy::I8 => (i8::MIN.into(), i8::MAX.into()),
92+
IntTy::I16 => (i16::MIN.into(), i16::MAX.into()),
93+
IntTy::I32 => (i32::MIN.into(), i32::MAX.into()),
94+
IntTy::I64 => (i64::MIN.into(), i64::MAX.into()),
95+
IntTy::I128 => (i128::MIN, i128::MAX),
96+
IntTy::Isize => (isize::MIN.try_into().ok()?, isize::MAX.try_into().ok()?),
97+
})
98+
}
99+
fn bits(self) -> Self::Output {
100+
match self {
101+
IntTy::I8 => 8,
102+
IntTy::I16 => 16,
103+
IntTy::I32 => 32,
104+
IntTy::I64 => 64,
105+
IntTy::I128 => 128,
106+
IntTy::Isize => isize::BITS.into(),
107+
}
108+
}
109+
}
110+
54111
impl<'tcx> PartialEq for Constant<'tcx> {
55112
fn eq(&self, other: &Self) -> bool {
56113
match (self, other) {
@@ -435,8 +492,15 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
435492
match *o {
436493
Int(value) => {
437494
let ty::Int(ity) = *ty.kind() else { return None };
495+
let (min, _) = ity.min_max()?;
438496
// sign extend
439497
let value = sext(self.lcx.tcx, value, ity);
498+
499+
// Applying unary - to the most negative value of any signed integer type panics.
500+
if value == min {
501+
return None;
502+
}
503+
440504
let value = value.checked_neg()?;
441505
// clear unused bits
442506
Some(Int(unsext(self.lcx.tcx, value, ity)))
@@ -572,17 +636,33 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
572636
match (l, r) {
573637
(Constant::Int(l), Some(Constant::Int(r))) => match *self.typeck_results.expr_ty_opt(left)?.kind() {
574638
ty::Int(ity) => {
639+
let (ty_min_value, _) = ity.min_max()?;
640+
let bits = ity.bits();
575641
let l = sext(self.lcx.tcx, l, ity);
576642
let r = sext(self.lcx.tcx, r, ity);
643+
644+
// Using / or %, where the left-hand argument is the smallest integer of a signed integer type and
645+
// the right-hand argument is -1 always panics, even with overflow-checks disabled
646+
if let BinOpKind::Div | BinOpKind::Rem = op.node
647+
&& l == ty_min_value
648+
&& r == -1
649+
{
650+
return None;
651+
}
652+
577653
let zext = |n: i128| Constant::Int(unsext(self.lcx.tcx, n, ity));
578654
match op.node {
579-
BinOpKind::Add => l.checked_add(r).map(zext),
580-
BinOpKind::Sub => l.checked_sub(r).map(zext),
581-
BinOpKind::Mul => l.checked_mul(r).map(zext),
655+
// When +, * or binary - create a value greater than the maximum value, or less than
656+
// the minimum value that can be stored, it panics.
657+
BinOpKind::Add => l.checked_add(r).and_then(|n| ity.ensure_fits(n)).map(zext),
658+
BinOpKind::Sub => l.checked_sub(r).and_then(|n| ity.ensure_fits(n)).map(zext),
659+
BinOpKind::Mul => l.checked_mul(r).and_then(|n| ity.ensure_fits(n)).map(zext),
582660
BinOpKind::Div if r != 0 => l.checked_div(r).map(zext),
583661
BinOpKind::Rem if r != 0 => l.checked_rem(r).map(zext),
584-
BinOpKind::Shr => l.checked_shr(r.try_into().ok()?).map(zext),
585-
BinOpKind::Shl => l.checked_shl(r.try_into().ok()?).map(zext),
662+
// Using << or >> where the right-hand argument is greater than or equal to the number of bits
663+
// in the type of the left-hand argument, or is negative panics.
664+
BinOpKind::Shr if r < bits && !r.is_negative() => l.checked_shr(r.try_into().ok()?).map(zext),
665+
BinOpKind::Shl if r < bits && !r.is_negative() => l.checked_shl(r.try_into().ok()?).map(zext),
586666
BinOpKind::BitXor => Some(zext(l ^ r)),
587667
BinOpKind::BitOr => Some(zext(l | r)),
588668
BinOpKind::BitAnd => Some(zext(l & r)),
@@ -595,24 +675,28 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
595675
_ => None,
596676
}
597677
},
598-
ty::Uint(_) => match op.node {
599-
BinOpKind::Add => l.checked_add(r).map(Constant::Int),
600-
BinOpKind::Sub => l.checked_sub(r).map(Constant::Int),
601-
BinOpKind::Mul => l.checked_mul(r).map(Constant::Int),
602-
BinOpKind::Div => l.checked_div(r).map(Constant::Int),
603-
BinOpKind::Rem => l.checked_rem(r).map(Constant::Int),
604-
BinOpKind::Shr => l.checked_shr(r.try_into().ok()?).map(Constant::Int),
605-
BinOpKind::Shl => l.checked_shl(r.try_into().ok()?).map(Constant::Int),
606-
BinOpKind::BitXor => Some(Constant::Int(l ^ r)),
607-
BinOpKind::BitOr => Some(Constant::Int(l | r)),
608-
BinOpKind::BitAnd => Some(Constant::Int(l & r)),
609-
BinOpKind::Eq => Some(Constant::Bool(l == r)),
610-
BinOpKind::Ne => Some(Constant::Bool(l != r)),
611-
BinOpKind::Lt => Some(Constant::Bool(l < r)),
612-
BinOpKind::Le => Some(Constant::Bool(l <= r)),
613-
BinOpKind::Ge => Some(Constant::Bool(l >= r)),
614-
BinOpKind::Gt => Some(Constant::Bool(l > r)),
615-
_ => None,
678+
ty::Uint(ity) => {
679+
let bits = ity.bits();
680+
681+
match op.node {
682+
BinOpKind::Add => l.checked_add(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
683+
BinOpKind::Sub => l.checked_sub(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
684+
BinOpKind::Mul => l.checked_mul(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
685+
BinOpKind::Div => l.checked_div(r).map(Constant::Int),
686+
BinOpKind::Rem => l.checked_rem(r).map(Constant::Int),
687+
BinOpKind::Shr if r < bits => l.checked_shr(r.try_into().ok()?).map(Constant::Int),
688+
BinOpKind::Shl if r < bits => l.checked_shl(r.try_into().ok()?).map(Constant::Int),
689+
BinOpKind::BitXor => Some(Constant::Int(l ^ r)),
690+
BinOpKind::BitOr => Some(Constant::Int(l | r)),
691+
BinOpKind::BitAnd => Some(Constant::Int(l & r)),
692+
BinOpKind::Eq => Some(Constant::Bool(l == r)),
693+
BinOpKind::Ne => Some(Constant::Bool(l != r)),
694+
BinOpKind::Lt => Some(Constant::Bool(l < r)),
695+
BinOpKind::Le => Some(Constant::Bool(l <= r)),
696+
BinOpKind::Ge => Some(Constant::Bool(l >= r)),
697+
BinOpKind::Gt => Some(Constant::Bool(l > r)),
698+
_ => None,
699+
}
616700
},
617701
_ => None,
618702
},

clippy_utils/src/eager_or_lazy.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
//! - or-fun-call
1010
//! - option-if-let-else
1111
12+
use crate::consts::{constant, FullInt};
1213
use crate::ty::{all_predicates_of, is_copy};
1314
use crate::visitors::is_const_evaluatable;
1415
use rustc_hir::def::{DefKind, Res};
1516
use rustc_hir::def_id::DefId;
1617
use rustc_hir::intravisit::{walk_expr, Visitor};
17-
use rustc_hir::{Block, Expr, ExprKind, QPath, UnOp};
18+
use rustc_hir::{BinOpKind, Block, Expr, ExprKind, QPath, UnOp};
1819
use rustc_lint::LateContext;
1920
use rustc_middle::ty;
2021
use rustc_middle::ty::adjustment::Adjust;
@@ -193,6 +194,12 @@ fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessS
193194
self.eagerness = Lazy;
194195
}
195196
},
197+
198+
// `-i32::MIN` panics with overflow checks, but `constant` lets us rule out some simple cases
199+
ExprKind::Unary(UnOp::Neg, _) if constant(self.cx, self.cx.typeck_results(), e).is_none() => {
200+
self.eagerness |= NoChange;
201+
},
202+
196203
// Custom `Deref` impl might have side effects
197204
ExprKind::Unary(UnOp::Deref, e)
198205
if self.cx.typeck_results().expr_ty(e).builtin_deref(true).is_none() =>
@@ -207,6 +214,37 @@ fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessS
207214
self.cx.typeck_results().expr_ty(e).kind(),
208215
ty::Bool | ty::Int(_) | ty::Uint(_),
209216
) => {},
217+
218+
// `>>` and `<<` panic when the right-hand side is greater than or equal to the number of bits in the
219+
// type of the left-hand side, or is negative. Doesn't need `constant` on the whole expression
220+
// because only the right side matters.
221+
ExprKind::Binary(op, left, right)
222+
if matches!(op.node, BinOpKind::Shl | BinOpKind::Shr)
223+
&& let left_ty = self.cx.typeck_results().expr_ty(left)
224+
&& let left_bits = left_ty.int_size_and_signed(self.cx.tcx).0.bits()
225+
&& constant(self.cx, self.cx.typeck_results(), right)
226+
.and_then(|c| c.int_value(self.cx, left_ty))
227+
.map_or(true, |c| match c {
228+
FullInt::S(i) => i >= i128::from(left_bits) || i.is_negative(),
229+
FullInt::U(i) => i >= u128::from(left_bits),
230+
}) =>
231+
{
232+
self.eagerness |= NoChange;
233+
},
234+
235+
// Arithmetic operations panic on under-/overflow with overflow checks, so don't suggest changing it:
236+
// https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#overflow
237+
// Using `constant` lets us allow some simple cases where we know for sure it can't overflow
238+
ExprKind::Binary(op, ..)
239+
if matches!(
240+
op.node,
241+
BinOpKind::Add | BinOpKind::Mul | BinOpKind::Sub | BinOpKind::Div | BinOpKind::Rem
242+
) && !self.cx.typeck_results().expr_ty(e).is_floating_point()
243+
&& constant(self.cx, self.cx.typeck_results(), e).is_none() =>
244+
{
245+
self.eagerness |= NoChange;
246+
},
247+
210248
ExprKind::Binary(_, lhs, rhs)
211249
if self.cx.typeck_results().expr_ty(lhs).is_primitive()
212250
&& self.cx.typeck_results().expr_ty(rhs).is_primitive() => {},

tests/ui/unnecessary_lazy_eval.fixed

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#![allow(clippy::needless_borrow)]
77
#![allow(clippy::unnecessary_literal_unwrap)]
88
#![allow(clippy::unit_arg)]
9+
#![allow(arithmetic_overflow)]
10+
#![allow(unconditional_panic)]
911

1012
use std::ops::Deref;
1113

@@ -183,10 +185,54 @@ fn main() {
183185
// neither bind_instead_of_map nor unnecessary_lazy_eval applies here
184186
let _: Result<usize, usize> = res.and_then(|x| Err(x));
185187
let _: Result<usize, usize> = res.or_else(|err| Ok(err));
188+
189+
issue9422(3);
190+
panicky_arithmetic_ops(3);
186191
}
187192

188193
#[allow(unused)]
189194
fn issue9485() {
190195
// should not lint, is in proc macro
191196
with_span!(span Some(42).unwrap_or_else(|| 2););
192197
}
198+
199+
fn issue9422(x: usize) -> Option<usize> {
200+
(x >= 5).then(|| x - 5)
201+
// (x >= 5).then_some(x - 5) // clippy suggestion panics
202+
}
203+
204+
// https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#overflow
205+
fn panicky_arithmetic_ops(x: usize) {
206+
let _x = false.then(|| i32::MAX + 1);
207+
let _x = false.then(|| i32::MAX * 2);
208+
let _x = false.then_some(i32::MAX - 1);
209+
//~^ ERROR: unnecessary closure used with `bool::then`
210+
let _x = false.then(|| i32::MIN - 1);
211+
#[allow(clippy::identity_op)]
212+
let _x = false.then_some((1 + 2 * 3 - 2 / 3 + 9) << 2);
213+
//~^ ERROR: unnecessary closure used with `bool::then`
214+
let _x = false.then_some(255u8 << 7);
215+
//~^ ERROR: unnecessary closure used with `bool::then`
216+
let _x = false.then(|| 255u8 << 8);
217+
let _x = false.then(|| 255u8 >> 8);
218+
let _x = false.then(|| 255u8 >> x);
219+
let _x = false.then(|| i32::MIN / -1);
220+
let _x = false.then_some(i32::MAX + -1);
221+
//~^ ERROR: unnecessary closure used with `bool::then`
222+
let _x = false.then_some(-i32::MAX);
223+
//~^ ERROR: unnecessary closure used with `bool::then`
224+
let _x = false.then(|| -i32::MIN);
225+
let _x = false.then(|| 255 >> -7);
226+
let _x = false.then(|| 255 << -1);
227+
let _x = false.then(|| 1 / 0);
228+
let _x = false.then(|| x << -1);
229+
let _x = false.then_some(x << 2);
230+
//~^ ERROR: unnecessary closure used with `bool::then`
231+
232+
// const eval doesn't read variables, but floating point math never panics, so we can still emit a
233+
// warning
234+
let f1 = 1.0;
235+
let f2 = 2.0;
236+
let _x = false.then_some(f1 + f2);
237+
//~^ ERROR: unnecessary closure used with `bool::then`
238+
}

tests/ui/unnecessary_lazy_eval.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#![allow(clippy::needless_borrow)]
77
#![allow(clippy::unnecessary_literal_unwrap)]
88
#![allow(clippy::unit_arg)]
9+
#![allow(arithmetic_overflow)]
10+
#![allow(unconditional_panic)]
911

1012
use std::ops::Deref;
1113

@@ -183,10 +185,54 @@ fn main() {
183185
// neither bind_instead_of_map nor unnecessary_lazy_eval applies here
184186
let _: Result<usize, usize> = res.and_then(|x| Err(x));
185187
let _: Result<usize, usize> = res.or_else(|err| Ok(err));
188+
189+
issue9422(3);
190+
panicky_arithmetic_ops(3);
186191
}
187192

188193
#[allow(unused)]
189194
fn issue9485() {
190195
// should not lint, is in proc macro
191196
with_span!(span Some(42).unwrap_or_else(|| 2););
192197
}
198+
199+
fn issue9422(x: usize) -> Option<usize> {
200+
(x >= 5).then(|| x - 5)
201+
// (x >= 5).then_some(x - 5) // clippy suggestion panics
202+
}
203+
204+
// https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#overflow
205+
fn panicky_arithmetic_ops(x: usize) {
206+
let _x = false.then(|| i32::MAX + 1);
207+
let _x = false.then(|| i32::MAX * 2);
208+
let _x = false.then(|| i32::MAX - 1);
209+
//~^ ERROR: unnecessary closure used with `bool::then`
210+
let _x = false.then(|| i32::MIN - 1);
211+
#[allow(clippy::identity_op)]
212+
let _x = false.then(|| (1 + 2 * 3 - 2 / 3 + 9) << 2);
213+
//~^ ERROR: unnecessary closure used with `bool::then`
214+
let _x = false.then(|| 255u8 << 7);
215+
//~^ ERROR: unnecessary closure used with `bool::then`
216+
let _x = false.then(|| 255u8 << 8);
217+
let _x = false.then(|| 255u8 >> 8);
218+
let _x = false.then(|| 255u8 >> x);
219+
let _x = false.then(|| i32::MIN / -1);
220+
let _x = false.then(|| i32::MAX + -1);
221+
//~^ ERROR: unnecessary closure used with `bool::then`
222+
let _x = false.then(|| -i32::MAX);
223+
//~^ ERROR: unnecessary closure used with `bool::then`
224+
let _x = false.then(|| -i32::MIN);
225+
let _x = false.then(|| 255 >> -7);
226+
let _x = false.then(|| 255 << -1);
227+
let _x = false.then(|| 1 / 0);
228+
let _x = false.then(|| x << -1);
229+
let _x = false.then(|| x << 2);
230+
//~^ ERROR: unnecessary closure used with `bool::then`
231+
232+
// const eval doesn't read variables, but floating point math never panics, so we can still emit a
233+
// warning
234+
let f1 = 1.0;
235+
let f2 = 2.0;
236+
let _x = false.then(|| f1 + f2);
237+
//~^ ERROR: unnecessary closure used with `bool::then`
238+
}

0 commit comments

Comments
 (0)