| use clippy_utils::diagnostics::span_lint; |
| use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node}; |
| use clippy_utils::sym; |
| use rustc_hir::intravisit::{Visitor, walk_expr}; |
| use rustc_hir::{BorrowKind, Expr, ExprKind, MatchSource, Mutability}; |
| use rustc_lint::{LateContext, LateLintPass}; |
| use rustc_middle::hir::nested_filter; |
| use rustc_middle::ty; |
| use rustc_session::declare_lint_pass; |
| use rustc_span::Span; |
| |
| declare_clippy_lint! { |
| /// ### What it does |
| /// Checks for function/method calls with a mutable |
| /// parameter in `debug_assert!`, `debug_assert_eq!` and `debug_assert_ne!` macros. |
| /// |
| /// ### Why is this bad? |
| /// In release builds `debug_assert!` macros are optimized out by the |
| /// compiler. |
| /// Therefore mutating something in a `debug_assert!` macro results in different behavior |
| /// between a release and debug build. |
| /// |
| /// ### Example |
| /// ```rust,ignore |
| /// debug_assert_eq!(vec![3].pop(), Some(3)); |
| /// |
| /// // or |
| /// |
| /// # let mut x = 5; |
| /// # fn takes_a_mut_parameter(_: &mut u32) -> bool { unimplemented!() } |
| /// debug_assert!(takes_a_mut_parameter(&mut x)); |
| /// ``` |
| #[clippy::version = "1.40.0"] |
| pub DEBUG_ASSERT_WITH_MUT_CALL, |
| nursery, |
| "mutable arguments in `debug_assert{,_ne,_eq}!`" |
| } |
| |
| declare_lint_pass!(DebugAssertWithMutCall => [DEBUG_ASSERT_WITH_MUT_CALL]); |
| |
| impl<'tcx> LateLintPass<'tcx> for DebugAssertWithMutCall { |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { |
| let Some(macro_call) = root_macro_call_first_node(cx, e) else { |
| return; |
| }; |
| if !matches!( |
| cx.tcx.get_diagnostic_name(macro_call.def_id), |
| Some(sym::debug_assert_macro | sym::debug_assert_eq_macro | sym::debug_assert_ne_macro) |
| ) { |
| return; |
| } |
| let Some((lhs, rhs, _)) = find_assert_eq_args(cx, e, macro_call.expn) else { |
| return; |
| }; |
| for arg in [lhs, rhs] { |
| let mut visitor = MutArgVisitor::new(cx); |
| visitor.visit_expr(arg); |
| if let Some(span) = visitor.expr_span() { |
| span_lint( |
| cx, |
| DEBUG_ASSERT_WITH_MUT_CALL, |
| span, |
| format!( |
| "do not call a function with mutable arguments inside of `{}!`", |
| cx.tcx.item_name(macro_call.def_id) |
| ), |
| ); |
| } |
| } |
| } |
| } |
| |
| struct MutArgVisitor<'a, 'tcx> { |
| cx: &'a LateContext<'tcx>, |
| expr_span: Option<Span>, |
| found: bool, |
| } |
| |
| impl<'a, 'tcx> MutArgVisitor<'a, 'tcx> { |
| fn new(cx: &'a LateContext<'tcx>) -> Self { |
| Self { |
| cx, |
| expr_span: None, |
| found: false, |
| } |
| } |
| |
| fn expr_span(&self) -> Option<Span> { |
| if self.found { self.expr_span } else { None } |
| } |
| } |
| |
| impl<'tcx> Visitor<'tcx> for MutArgVisitor<'_, 'tcx> { |
| type NestedFilter = nested_filter::OnlyBodies; |
| |
| fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { |
| match expr.kind { |
| ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) => { |
| self.found = true; |
| return; |
| }, |
| ExprKind::Path(_) => { |
| if let Some(adj) = self.cx.typeck_results().adjustments().get(expr.hir_id) |
| && adj |
| .iter() |
| .any(|a| matches!(a.target.kind(), ty::Ref(_, _, Mutability::Mut))) |
| { |
| self.found = true; |
| return; |
| } |
| }, |
| // Don't check await desugars |
| ExprKind::Match(_, _, MatchSource::AwaitDesugar) => return, |
| _ if !self.found => self.expr_span = Some(expr.span), |
| _ => return, |
| } |
| walk_expr(self, expr); |
| } |
| |
| fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| self.cx.tcx |
| } |
| } |