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

teach eager_or_lazy about panicky arithmetic operations #11002

Merged
merged 5 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions clippy_lints/src/matches/match_same_arms.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::diagnostics::span_lint_hir_and_then;
use clippy_utils::source::snippet;
use clippy_utils::{is_lint_allowed, path_to_local, search_same, SpanlessEq, SpanlessHash};
use core::cmp::Ordering;
Expand Down Expand Up @@ -106,9 +106,10 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) {
if !cx.tcx.features().non_exhaustive_omitted_patterns_lint
|| is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, arm2.hir_id)
{
span_lint_and_then(
span_lint_hir_and_then(
cx,
MATCH_SAME_ARMS,
arm1.hir_id,
arm1.span,
"this match arm has an identical body to the `_` wildcard arm",
|diag| {
Expand All @@ -126,9 +127,10 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) {
(arm2, arm1)
};

span_lint_and_then(
span_lint_hir_and_then(
cx,
MATCH_SAME_ARMS,
keep_arm.hir_id,
keep_arm.span,
"this match arm has an identical body to another arm",
|diag| {
Expand Down
132 changes: 108 additions & 24 deletions clippy_utils/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rustc_hir::{BinOp, BinOpKind, Block, ConstBlock, Expr, ExprKind, HirId, Item
use rustc_lexer::tokenize;
use rustc_lint::LateContext;
use rustc_middle::mir::interpret::{alloc_range, Scalar};
use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, List, ScalarInt, Ty, TyCtxt};
use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, IntTy, List, ScalarInt, Ty, TyCtxt, UintTy};
use rustc_middle::{bug, mir, span_bug};
use rustc_span::symbol::{Ident, Symbol};
use rustc_span::SyntaxContext;
Expand Down Expand Up @@ -51,6 +51,63 @@ pub enum Constant<'tcx> {
Err,
}

trait IntTypeBounds: Sized {
type Output: PartialOrd;

fn min_max(self) -> Option<(Self::Output, Self::Output)>;
fn bits(self) -> Self::Output;
fn ensure_fits(self, val: Self::Output) -> Option<Self::Output> {
let (min, max) = self.min_max()?;
(min <= val && val <= max).then_some(val)
}
}
impl IntTypeBounds for UintTy {
type Output = u128;
fn min_max(self) -> Option<(Self::Output, Self::Output)> {
Some(match self {
UintTy::U8 => (u8::MIN.into(), u8::MAX.into()),
UintTy::U16 => (u16::MIN.into(), u16::MAX.into()),
UintTy::U32 => (u32::MIN.into(), u32::MAX.into()),
UintTy::U64 => (u64::MIN.into(), u64::MAX.into()),
UintTy::U128 => (u128::MIN, u128::MAX),
UintTy::Usize => (usize::MIN.try_into().ok()?, usize::MAX.try_into().ok()?),
})
}
fn bits(self) -> Self::Output {
match self {
UintTy::U8 => 8,
UintTy::U16 => 16,
UintTy::U32 => 32,
UintTy::U64 => 64,
UintTy::U128 => 128,
UintTy::Usize => usize::BITS.into(),
}
}
}
impl IntTypeBounds for IntTy {
type Output = i128;
fn min_max(self) -> Option<(Self::Output, Self::Output)> {
Some(match self {
IntTy::I8 => (i8::MIN.into(), i8::MAX.into()),
IntTy::I16 => (i16::MIN.into(), i16::MAX.into()),
IntTy::I32 => (i32::MIN.into(), i32::MAX.into()),
IntTy::I64 => (i64::MIN.into(), i64::MAX.into()),
IntTy::I128 => (i128::MIN, i128::MAX),
IntTy::Isize => (isize::MIN.try_into().ok()?, isize::MAX.try_into().ok()?),
})
}
fn bits(self) -> Self::Output {
match self {
IntTy::I8 => 8,
IntTy::I16 => 16,
IntTy::I32 => 32,
IntTy::I64 => 64,
IntTy::I128 => 128,
IntTy::Isize => isize::BITS.into(),
}
}
}

impl<'tcx> PartialEq for Constant<'tcx> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
Expand Down Expand Up @@ -435,8 +492,15 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
match *o {
Int(value) => {
let ty::Int(ity) = *ty.kind() else { return None };
let (min, _) = ity.min_max()?;
// sign extend
let value = sext(self.lcx.tcx, value, ity);

// Applying unary - to the most negative value of any signed integer type panics.
if value == min {
return None;
}

let value = value.checked_neg()?;
// clear unused bits
Some(Int(unsext(self.lcx.tcx, value, ity)))
Expand Down Expand Up @@ -572,17 +636,33 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
match (l, r) {
(Constant::Int(l), Some(Constant::Int(r))) => match *self.typeck_results.expr_ty_opt(left)?.kind() {
ty::Int(ity) => {
let (ty_min_value, _) = ity.min_max()?;
let bits = ity.bits();
let l = sext(self.lcx.tcx, l, ity);
let r = sext(self.lcx.tcx, r, ity);

// Using / or %, where the left-hand argument is the smallest integer of a signed integer type and
// the right-hand argument is -1 always panics, even with overflow-checks disabled
if let BinOpKind::Div | BinOpKind::Rem = op.node
&& l == ty_min_value
&& r == -1
{
return None;
}

let zext = |n: i128| Constant::Int(unsext(self.lcx.tcx, n, ity));
match op.node {
BinOpKind::Add => l.checked_add(r).map(zext),
BinOpKind::Sub => l.checked_sub(r).map(zext),
BinOpKind::Mul => l.checked_mul(r).map(zext),
// When +, * or binary - create a value greater than the maximum value, or less than
// the minimum value that can be stored, it panics.
BinOpKind::Add => l.checked_add(r).and_then(|n| ity.ensure_fits(n)).map(zext),
BinOpKind::Sub => l.checked_sub(r).and_then(|n| ity.ensure_fits(n)).map(zext),
BinOpKind::Mul => l.checked_mul(r).and_then(|n| ity.ensure_fits(n)).map(zext),
BinOpKind::Div if r != 0 => l.checked_div(r).map(zext),
BinOpKind::Rem if r != 0 => l.checked_rem(r).map(zext),
BinOpKind::Shr => l.checked_shr(r.try_into().ok()?).map(zext),
BinOpKind::Shl => l.checked_shl(r.try_into().ok()?).map(zext),
// Using << or >> where the right-hand argument is greater than or equal to the number of bits
// in the type of the left-hand argument, or is negative panics.
BinOpKind::Shr if r < bits && !r.is_negative() => l.checked_shr(r.try_into().ok()?).map(zext),
BinOpKind::Shl if r < bits && !r.is_negative() => l.checked_shl(r.try_into().ok()?).map(zext),
BinOpKind::BitXor => Some(zext(l ^ r)),
BinOpKind::BitOr => Some(zext(l | r)),
BinOpKind::BitAnd => Some(zext(l & r)),
Expand All @@ -595,24 +675,28 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
_ => None,
}
},
ty::Uint(_) => match op.node {
BinOpKind::Add => l.checked_add(r).map(Constant::Int),
BinOpKind::Sub => l.checked_sub(r).map(Constant::Int),
BinOpKind::Mul => l.checked_mul(r).map(Constant::Int),
BinOpKind::Div => l.checked_div(r).map(Constant::Int),
BinOpKind::Rem => l.checked_rem(r).map(Constant::Int),
BinOpKind::Shr => l.checked_shr(r.try_into().ok()?).map(Constant::Int),
BinOpKind::Shl => l.checked_shl(r.try_into().ok()?).map(Constant::Int),
BinOpKind::BitXor => Some(Constant::Int(l ^ r)),
BinOpKind::BitOr => Some(Constant::Int(l | r)),
BinOpKind::BitAnd => Some(Constant::Int(l & r)),
BinOpKind::Eq => Some(Constant::Bool(l == r)),
BinOpKind::Ne => Some(Constant::Bool(l != r)),
BinOpKind::Lt => Some(Constant::Bool(l < r)),
BinOpKind::Le => Some(Constant::Bool(l <= r)),
BinOpKind::Ge => Some(Constant::Bool(l >= r)),
BinOpKind::Gt => Some(Constant::Bool(l > r)),
_ => None,
ty::Uint(ity) => {
let bits = ity.bits();

match op.node {
BinOpKind::Add => l.checked_add(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
BinOpKind::Sub => l.checked_sub(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
BinOpKind::Mul => l.checked_mul(r).and_then(|n| ity.ensure_fits(n)).map(Constant::Int),
BinOpKind::Div => l.checked_div(r).map(Constant::Int),
BinOpKind::Rem => l.checked_rem(r).map(Constant::Int),
BinOpKind::Shr if r < bits => l.checked_shr(r.try_into().ok()?).map(Constant::Int),
BinOpKind::Shl if r < bits => l.checked_shl(r.try_into().ok()?).map(Constant::Int),
BinOpKind::BitXor => Some(Constant::Int(l ^ r)),
BinOpKind::BitOr => Some(Constant::Int(l | r)),
BinOpKind::BitAnd => Some(Constant::Int(l & r)),
BinOpKind::Eq => Some(Constant::Bool(l == r)),
BinOpKind::Ne => Some(Constant::Bool(l != r)),
BinOpKind::Lt => Some(Constant::Bool(l < r)),
BinOpKind::Le => Some(Constant::Bool(l <= r)),
BinOpKind::Ge => Some(Constant::Bool(l >= r)),
BinOpKind::Gt => Some(Constant::Bool(l > r)),
_ => None,
}
},
_ => None,
},
Expand Down
56 changes: 55 additions & 1 deletion clippy_utils/src/eager_or_lazy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
//! - or-fun-call
//! - option-if-let-else

use crate::consts::{constant, FullInt};
use crate::ty::{all_predicates_of, is_copy};
use crate::visitors::is_const_evaluatable;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::intravisit::{walk_expr, Visitor};
use rustc_hir::{Block, Expr, ExprKind, QPath, UnOp};
use rustc_hir::{BinOpKind, Block, Expr, ExprKind, QPath, UnOp};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_middle::ty::adjustment::Adjust;
Expand Down Expand Up @@ -193,6 +194,12 @@ fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessS
self.eagerness = Lazy;
}
},

// `-i32::MIN` panics with overflow checks
ExprKind::Unary(UnOp::Neg, right) if constant(self.cx, self.cx.typeck_results(), right).is_none() => {
self.eagerness |= NoChange;
},

// Custom `Deref` impl might have side effects
ExprKind::Unary(UnOp::Deref, e)
if self.cx.typeck_results().expr_ty(e).builtin_deref(true).is_none() =>
Expand All @@ -207,6 +214,53 @@ fn expr_eagerness<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> EagernessS
self.cx.typeck_results().expr_ty(e).kind(),
ty::Bool | ty::Int(_) | ty::Uint(_),
) => {},

// `>>` and `<<` panic when the right-hand side is greater than or equal to the number of bits in the
// type of the left-hand side, or is negative.
// We intentionally only check if the right-hand isn't a constant, because even if the suggestion would
// overflow with constants, the compiler emits an error for it and the programmer will have to fix it.
// Thus, we would realistically only delay the lint.
ExprKind::Binary(op, _, right)
if matches!(op.node, BinOpKind::Shl | BinOpKind::Shr)
&& constant(self.cx, self.cx.typeck_results(), right).is_none() =>
{
self.eagerness |= NoChange;
},

ExprKind::Binary(op, left, right)
if matches!(op.node, BinOpKind::Div | BinOpKind::Rem)
&& let left_ty = self.cx.typeck_results().expr_ty(left)
&& let right_ty = self.cx.typeck_results().expr_ty(right)
&& let left = constant(self.cx, self.cx.typeck_results(), left)
.and_then(|c| c.int_value(self.cx, left_ty))
&& let right = constant(self.cx, self.cx.typeck_results(), right)
.and_then(|c| c.int_value(self.cx, right_ty))
&& match (left, right) {
// `1 / x` -- x might be zero
(_, None) => true,
// `x / -1` -- x might be T::MIN = panic
#[expect(clippy::match_same_arms)]
(None, Some(FullInt::S(-1))) => true,
// anything else is either fine or checked by the compiler
_ => false,
} =>
y21 marked this conversation as resolved.
Show resolved Hide resolved
{
self.eagerness |= NoChange;
},

// Similar to `>>` and `<<`, we only want to avoid linting entirely if either side is unknown and the
// compiler can't emit an error for an overflowing expression.
// Suggesting eagerness for `true.then(|| i32::MAX + 1)` is okay because the compiler will emit an
// error and it's good to have the eagerness warning up front when the user fixes the logic error.
ExprKind::Binary(op, left, right)
if matches!(op.node, BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul)
&& !self.cx.typeck_results().expr_ty(e).is_floating_point()
&& (constant(self.cx, self.cx.typeck_results(), left).is_none()
|| constant(self.cx, self.cx.typeck_results(), right).is_none()) =>
{
self.eagerness |= NoChange;
},

ExprKind::Binary(_, lhs, rhs)
if self.cx.typeck_results().expr_ty(lhs).is_primitive()
&& self.cx.typeck_results().expr_ty(rhs).is_primitive() => {},
Expand Down
78 changes: 78 additions & 0 deletions tests/ui/unnecessary_lazy_eval.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#![allow(clippy::needless_borrow)]
#![allow(clippy::unnecessary_literal_unwrap)]
#![allow(clippy::unit_arg)]
#![allow(arithmetic_overflow)]
#![allow(unconditional_panic)]

use std::ops::Deref;

Expand Down Expand Up @@ -190,3 +192,79 @@ fn issue9485() {
// should not lint, is in proc macro
with_span!(span Some(42).unwrap_or_else(|| 2););
}

fn issue9422(x: usize) -> Option<usize> {
(x >= 5).then(|| x - 5)
// (x >= 5).then_some(x - 5) // clippy suggestion panics
}

fn panicky_arithmetic_ops(x: usize, y: isize) {
#![allow(clippy::identity_op, clippy::eq_op)]

// See comments in `eager_or_lazy.rs` for the rules that this is meant to follow

let _x = false.then_some(i32::MAX + 1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(i32::MAX * 2);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(i32::MAX - 1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(i32::MIN - 1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some((1 + 2 * 3 - 2 / 3 + 9) << 2);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(255u8 << 7);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(255u8 << 8);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(255u8 >> 8);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| 255u8 >> x);
let _x = false.then_some(i32::MAX + -1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(-i32::MAX);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(-i32::MIN);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| -y);
let _x = false.then_some(255 >> -7);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(255 << -1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(1 / 0);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(x << -1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(x << 2);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| x + x);
let _x = false.then(|| x * x);
let _x = false.then(|| x - x);
let _x = false.then(|| x / x);
let _x = false.then(|| x % x);
let _x = false.then(|| x + 1);
let _x = false.then(|| 1 + x);

let _x = false.then_some(x / 0);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(x % 0);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| y / -1);
let _x = false.then_some(1 / -1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(i32::MIN / -1);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| i32::MIN / x as i32);
let _x = false.then_some(i32::MIN / 0);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then_some(4 / 2);
//~^ ERROR: unnecessary closure used with `bool::then`
let _x = false.then(|| 1 / x);

// const eval doesn't read variables, but floating point math never panics, so we can still emit a
// warning
let f1 = 1.0;
let f2 = 2.0;
let _x = false.then_some(f1 + f2);
//~^ ERROR: unnecessary closure used with `bool::then`
}