1 use clippy_utils::diagnostics::span_lint; 2 use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node}; 3 use rustc_hir::intravisit::{walk_expr, Visitor}; 4 use rustc_hir::{BorrowKind, Expr, ExprKind, MatchSource, Mutability}; 5 use rustc_lint::{LateContext, LateLintPass}; 6 use rustc_middle::hir::nested_filter; 7 use rustc_middle::ty; 8 use rustc_session::{declare_lint_pass, declare_tool_lint}; 9 use rustc_span::Span; 10 11 declare_clippy_lint! { 12 /// ### What it does 13 /// Checks for function/method calls with a mutable 14 /// parameter in `debug_assert!`, `debug_assert_eq!` and `debug_assert_ne!` macros. 15 /// 16 /// ### Why is this bad? 17 /// In release builds `debug_assert!` macros are optimized out by the 18 /// compiler. 19 /// Therefore mutating something in a `debug_assert!` macro results in different behavior 20 /// between a release and debug build. 21 /// 22 /// ### Example 23 /// ```rust,ignore 24 /// debug_assert_eq!(vec![3].pop(), Some(3)); 25 /// 26 /// // or 27 /// 28 /// # let mut x = 5; 29 /// # fn takes_a_mut_parameter(_: &mut u32) -> bool { unimplemented!() } 30 /// debug_assert!(takes_a_mut_parameter(&mut x)); 31 /// ``` 32 #[clippy::version = "1.40.0"] 33 pub DEBUG_ASSERT_WITH_MUT_CALL, 34 nursery, 35 "mutable arguments in `debug_assert{,_ne,_eq}!`" 36 } 37 38 declare_lint_pass!(DebugAssertWithMutCall => [DEBUG_ASSERT_WITH_MUT_CALL]); 39 40 impl<'tcx> LateLintPass<'tcx> for DebugAssertWithMutCall { check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>)41 fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { 42 let Some(macro_call) = root_macro_call_first_node(cx, e) else { return }; 43 let macro_name = cx.tcx.item_name(macro_call.def_id); 44 if !matches!( 45 macro_name.as_str(), 46 "debug_assert" | "debug_assert_eq" | "debug_assert_ne" 47 ) { 48 return; 49 } 50 let Some((lhs, rhs, _)) = find_assert_eq_args(cx, e, macro_call.expn) else { return }; 51 for arg in [lhs, rhs] { 52 let mut visitor = MutArgVisitor::new(cx); 53 visitor.visit_expr(arg); 54 if let Some(span) = visitor.expr_span() { 55 span_lint( 56 cx, 57 DEBUG_ASSERT_WITH_MUT_CALL, 58 span, 59 &format!("do not call a function with mutable arguments inside of `{macro_name}!`"), 60 ); 61 } 62 } 63 } 64 } 65 66 struct MutArgVisitor<'a, 'tcx> { 67 cx: &'a LateContext<'tcx>, 68 expr_span: Option<Span>, 69 found: bool, 70 } 71 72 impl<'a, 'tcx> MutArgVisitor<'a, 'tcx> { new(cx: &'a LateContext<'tcx>) -> Self73 fn new(cx: &'a LateContext<'tcx>) -> Self { 74 Self { 75 cx, 76 expr_span: None, 77 found: false, 78 } 79 } 80 expr_span(&self) -> Option<Span>81 fn expr_span(&self) -> Option<Span> { 82 if self.found { self.expr_span } else { None } 83 } 84 } 85 86 impl<'a, 'tcx> Visitor<'tcx> for MutArgVisitor<'a, 'tcx> { 87 type NestedFilter = nested_filter::OnlyBodies; 88 visit_expr(&mut self, expr: &'tcx Expr<'_>)89 fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { 90 match expr.kind { 91 ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) => { 92 self.found = true; 93 return; 94 }, 95 ExprKind::If(..) => { 96 self.found = true; 97 return; 98 }, 99 ExprKind::Path(_) => { 100 if let Some(adj) = self.cx.typeck_results().adjustments().get(expr.hir_id) { 101 if adj 102 .iter() 103 .any(|a| matches!(a.target.kind(), ty::Ref(_, _, Mutability::Mut))) 104 { 105 self.found = true; 106 return; 107 } 108 } 109 }, 110 // Don't check await desugars 111 ExprKind::Match(_, _, MatchSource::AwaitDesugar) => return, 112 _ if !self.found => self.expr_span = Some(expr.span), 113 _ => return, 114 } 115 walk_expr(self, expr); 116 } 117 nested_visit_map(&mut self) -> Self::Map118 fn nested_visit_map(&mut self) -> Self::Map { 119 self.cx.tcx.hir() 120 } 121 } 122