blob: 4c6462b7747b4194a0c9687cd62b4f08b631cc12 [file] [log] [blame]
use super::ARITHMETIC_SIDE_EFFECTS;
use clippy_utils::consts::{constant, constant_simple, Constant};
use clippy_utils::diagnostics::span_lint;
use clippy_utils::ty::type_diagnostic_name;
use clippy_utils::{expr_or_init, is_from_proc_macro, is_lint_allowed, peel_hir_expr_refs, peel_hir_expr_unary};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::Ty;
use rustc_session::impl_lint_pass;
use rustc_span::{Span, Symbol};
use rustc_span::source_map::Spanned;
use rustc_span::symbol::sym;
use {rustc_ast as ast, rustc_hir as hir};
const HARD_CODED_ALLOWED_BINARY: &[[&str; 2]] = &[["f32", "f32"], ["f64", "f64"], ["std::string::String", "str"]];
const HARD_CODED_ALLOWED_UNARY: &[&str] = &["f32", "f64", "std::num::Saturating", "std::num::Wrapping"];
const INTEGER_METHODS: &[Symbol] = &[
sym::saturating_div,
sym::wrapping_div,
sym::wrapping_rem,
sym::wrapping_rem_euclid,
];
#[derive(Debug)]
pub struct ArithmeticSideEffects {
allowed_binary: FxHashMap<String, FxHashSet<String>>,
allowed_unary: FxHashSet<String>,
// Used to check whether expressions are constants, such as in enum discriminants and consts
const_span: Option<Span>,
expr_span: Option<Span>,
integer_methods: FxHashSet<Symbol>,
}
impl_lint_pass!(ArithmeticSideEffects => [ARITHMETIC_SIDE_EFFECTS]);
impl ArithmeticSideEffects {
#[must_use]
pub fn new(user_allowed_binary: Vec<[String; 2]>, user_allowed_unary: Vec<String>) -> Self {
let mut allowed_binary: FxHashMap<String, FxHashSet<String>> = <_>::default();
for [lhs, rhs] in user_allowed_binary.into_iter().chain(
HARD_CODED_ALLOWED_BINARY
.iter()
.copied()
.map(|[lhs, rhs]| [lhs.to_string(), rhs.to_string()]),
) {
allowed_binary.entry(lhs).or_default().insert(rhs);
}
let allowed_unary = user_allowed_unary
.into_iter()
.chain(HARD_CODED_ALLOWED_UNARY.iter().copied().map(String::from))
.collect();
Self {
allowed_binary,
allowed_unary,
const_span: None,
expr_span: None,
integer_methods: INTEGER_METHODS.iter().copied().collect(),
}
}
/// Checks if the lhs and the rhs types of a binary operation like "addition" or
/// "multiplication" are present in the inner set of allowed types.
fn has_allowed_binary(&self, lhs_ty: Ty<'_>, rhs_ty: Ty<'_>) -> bool {
let lhs_ty_string = lhs_ty.to_string();
let lhs_ty_string_elem = lhs_ty_string.split('<').next().unwrap_or_default();
let rhs_ty_string = rhs_ty.to_string();
let rhs_ty_string_elem = rhs_ty_string.split('<').next().unwrap_or_default();
if let Some(rhs_from_specific) = self.allowed_binary.get(lhs_ty_string_elem)
&& {
let rhs_has_allowed_ty = rhs_from_specific.contains(rhs_ty_string_elem);
rhs_has_allowed_ty || rhs_from_specific.contains("*")
}
{
true
} else if let Some(rhs_from_glob) = self.allowed_binary.get("*") {
rhs_from_glob.contains(rhs_ty_string_elem)
} else {
false
}
}
/// Checks if the type of an unary operation like "negation" is present in the inner set of
/// allowed types.
fn has_allowed_unary(&self, ty: Ty<'_>) -> bool {
let ty_string = ty.to_string();
let ty_string_elem = ty_string.split('<').next().unwrap_or_default();
self.allowed_unary.contains(ty_string_elem)
}
/// Verifies built-in types that have specific allowed operations
fn has_specific_allowed_type_and_operation(
cx: &LateContext<'_>,
lhs_ty: Ty<'_>,
op: &Spanned<hir::BinOpKind>,
rhs_ty: Ty<'_>,
) -> bool {
let is_div_or_rem = matches!(op.node, hir::BinOpKind::Div | hir::BinOpKind::Rem);
let is_non_zero_u = |symbol: Option<Symbol>| {
matches!(
symbol,
Some(
sym::NonZeroU128
| sym::NonZeroU16
| sym::NonZeroU32
| sym::NonZeroU64
| sym::NonZeroU8
| sym::NonZeroUsize
)
)
};
let is_sat_or_wrap = |ty: Ty<'_>| {
let is_sat = type_diagnostic_name(cx, ty) == Some(sym::Saturating);
let is_wrap = type_diagnostic_name(cx, ty) == Some(sym::Wrapping);
is_sat || is_wrap
};
// If the RHS is NonZeroU*, then division or module by zero will never occur
if is_non_zero_u(type_diagnostic_name(cx, rhs_ty)) && is_div_or_rem {
return true;
}
// `Saturation` and `Wrapping` can overflow if the RHS is zero in a division or module
if is_sat_or_wrap(lhs_ty) {
return !is_div_or_rem;
}
false
}
// For example, 8i32 or &i64::MAX.
fn is_integral(ty: Ty<'_>) -> bool {
ty.peel_refs().is_integral()
}
// Common entry-point to avoid code duplication.
fn issue_lint<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
if is_from_proc_macro(cx, expr) {
return;
}
let msg = "arithmetic operation that can potentially result in unexpected side-effects";
span_lint(cx, ARITHMETIC_SIDE_EFFECTS, expr.span, msg);
self.expr_span = Some(expr.span);
}
/// Returns the numeric value of a literal integer originated from `expr`, if any.
///
/// Literal integers can be originated from adhoc declarations like `1`, associated constants
/// like `i32::MAX` or constant references like `N` from `const N: i32 = 1;`,
fn literal_integer(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<u128> {
let actual = peel_hir_expr_unary(expr).0;
if let hir::ExprKind::Lit(lit) = actual.kind
&& let ast::LitKind::Int(n, _) = lit.node
{
return Some(n);
}
if let Some(Constant::Int(n)) = constant(cx, cx.typeck_results(), expr) {
return Some(n);
}
None
}
/// Manages when the lint should be triggered. Operations in constant environments, hard coded
/// types, custom allowed types and non-constant operations that won't overflow are ignored.
fn manage_bin_ops<'tcx>(
&mut self,
cx: &LateContext<'tcx>,
expr: &'tcx hir::Expr<'_>,
op: &Spanned<hir::BinOpKind>,
lhs: &'tcx hir::Expr<'_>,
rhs: &'tcx hir::Expr<'_>,
) {
if constant_simple(cx, cx.typeck_results(), expr).is_some() {
return;
}
if !matches!(
op.node,
hir::BinOpKind::Add
| hir::BinOpKind::Div
| hir::BinOpKind::Mul
| hir::BinOpKind::Rem
| hir::BinOpKind::Shl
| hir::BinOpKind::Shr
| hir::BinOpKind::Sub
) {
return;
};
let (mut actual_lhs, lhs_ref_counter) = peel_hir_expr_refs(lhs);
let (mut actual_rhs, rhs_ref_counter) = peel_hir_expr_refs(rhs);
actual_lhs = expr_or_init(cx, actual_lhs);
actual_rhs = expr_or_init(cx, actual_rhs);
let lhs_ty = cx.typeck_results().expr_ty(actual_lhs).peel_refs();
let rhs_ty = cx.typeck_results().expr_ty(actual_rhs).peel_refs();
if self.has_allowed_binary(lhs_ty, rhs_ty) {
return;
}
if Self::has_specific_allowed_type_and_operation(cx, lhs_ty, op, rhs_ty) {
return;
}
let has_valid_op = if Self::is_integral(lhs_ty) && Self::is_integral(rhs_ty) {
if let hir::BinOpKind::Shl | hir::BinOpKind::Shr = op.node {
// At least for integers, shifts are already handled by the CTFE
return;
}
match (
Self::literal_integer(cx, actual_lhs),
Self::literal_integer(cx, actual_rhs),
) {
(None, None) => false,
(None, Some(n)) => match (&op.node, n) {
// Division and module are always valid if applied to non-zero integers
(hir::BinOpKind::Div | hir::BinOpKind::Rem, local_n) if local_n != 0 => true,
// Adding or subtracting zeros is always a no-op
(hir::BinOpKind::Add | hir::BinOpKind::Sub, 0)
// Multiplication by 1 or 0 will never overflow
| (hir::BinOpKind::Mul, 0 | 1)
=> true,
_ => false,
},
(Some(n), None) => match (&op.node, n) {
// Adding or subtracting zeros is always a no-op
(hir::BinOpKind::Add | hir::BinOpKind::Sub, 0)
// Multiplication by 1 or 0 will never overflow
| (hir::BinOpKind::Mul, 0 | 1)
=> true,
_ => false,
},
(Some(_), Some(_)) => {
matches!((lhs_ref_counter, rhs_ref_counter), (0, 0))
},
}
} else {
false
};
if !has_valid_op {
self.issue_lint(cx, expr);
}
}
/// There are some integer methods like `wrapping_div` that will panic depending on the
/// provided input.
fn manage_method_call<'tcx>(
&mut self,
args: &'tcx [hir::Expr<'_>],
cx: &LateContext<'tcx>,
ps: &'tcx hir::PathSegment<'_>,
receiver: &'tcx hir::Expr<'_>,
) {
let Some(arg) = args.first() else {
return;
};
if constant_simple(cx, cx.typeck_results(), receiver).is_some() {
return;
}
let instance_ty = cx.typeck_results().expr_ty(receiver);
if !Self::is_integral(instance_ty) {
return;
}
if !self.integer_methods.contains(&ps.ident.name) {
return;
}
let (actual_arg, _) = peel_hir_expr_refs(arg);
match Self::literal_integer(cx, actual_arg) {
None | Some(0) => self.issue_lint(cx, arg),
Some(_) => {},
}
}
fn manage_unary_ops<'tcx>(
&mut self,
cx: &LateContext<'tcx>,
expr: &'tcx hir::Expr<'_>,
un_expr: &'tcx hir::Expr<'_>,
un_op: hir::UnOp,
) {
let hir::UnOp::Neg = un_op else {
return;
};
if constant(cx, cx.typeck_results(), un_expr).is_some() {
return;
}
let ty = cx.typeck_results().expr_ty(expr).peel_refs();
if self.has_allowed_unary(ty) {
return;
}
let actual_un_expr = peel_hir_expr_refs(un_expr).0;
if Self::literal_integer(cx, actual_un_expr).is_some() {
return;
}
self.issue_lint(cx, expr);
}
fn should_skip_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) -> bool {
is_lint_allowed(cx, ARITHMETIC_SIDE_EFFECTS, expr.hir_id)
|| self.expr_span.is_some()
|| self.const_span.map_or(false, |sp| sp.contains(expr.span))
}
}
impl<'tcx> LateLintPass<'tcx> for ArithmeticSideEffects {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
if self.should_skip_expr(cx, expr) {
return;
}
match &expr.kind {
hir::ExprKind::AssignOp(op, lhs, rhs) | hir::ExprKind::Binary(op, lhs, rhs) => {
self.manage_bin_ops(cx, expr, op, lhs, rhs);
},
hir::ExprKind::MethodCall(ps, receiver, args, _) => {
self.manage_method_call(args, cx, ps, receiver);
},
hir::ExprKind::Unary(un_op, un_expr) => {
self.manage_unary_ops(cx, expr, un_expr, *un_op);
},
_ => {},
}
}
fn check_body(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) {
let body_owner = cx.tcx.hir().body_owner(body.id());
let body_owner_def_id = cx.tcx.hir().body_owner_def_id(body.id());
let body_owner_kind = cx.tcx.hir().body_owner_kind(body_owner_def_id);
if let hir::BodyOwnerKind::Const { .. } | hir::BodyOwnerKind::Static(_) = body_owner_kind {
let body_span = cx.tcx.hir().span_with_body(body_owner);
if let Some(span) = self.const_span
&& span.contains(body_span)
{
return;
}
self.const_span = Some(body_span);
}
}
fn check_body_post(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) {
let body_owner = cx.tcx.hir().body_owner(body.id());
let body_span = cx.tcx.hir().span(body_owner);
if let Some(span) = self.const_span
&& span.contains(body_span)
{
return;
}
self.const_span = None;
}
fn check_expr_post(&mut self, _: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
if Some(expr.span) == self.expr_span {
self.expr_span = None;
}
}
}