use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::macros::{find_assert_args, find_assert_eq_args, root_macro_call_first_node, PanicExpn}; use clippy_utils::{is_in_cfg_test, is_in_test_function}; use rustc_hir::Expr; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::sym; declare_clippy_lint! { /// ### What it does /// Checks assertions without a custom panic message. /// /// ### Why is this bad? /// Without a good custom message, it'd be hard to understand what went wrong when the assertion fails. /// A good custom message should be more about why the failure of the assertion is problematic /// and not what is failed because the assertion already conveys that. /// /// Although the same reasoning applies to testing functions, this lint ignores them as they would be too noisy. /// Also, in most cases understanding the test failure would be easier /// compared to understanding a complex invariant distributed around the codebase. /// /// ### Known problems /// This lint cannot check the quality of the custom panic messages. /// Hence, you can suppress this lint simply by adding placeholder messages /// like "assertion failed". However, we recommend coming up with good messages /// that provide useful information instead of placeholder messages that /// don't provide any extra information. /// /// ### Example /// ```no_run /// # struct Service { ready: bool } /// fn call(service: Service) { /// assert!(service.ready); /// } /// ``` /// Use instead: /// ```no_run /// # struct Service { ready: bool } /// fn call(service: Service) { /// assert!(service.ready, "`service.poll_ready()` must be called first to ensure that service is ready to receive requests"); /// } /// ``` #[clippy::version = "1.70.0"] pub MISSING_ASSERT_MESSAGE, restriction, "checks assertions without a custom panic message" } declare_lint_pass!(MissingAssertMessage => [MISSING_ASSERT_MESSAGE]); impl<'tcx> LateLintPass<'tcx> for MissingAssertMessage { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return; }; let single_argument = match cx.tcx.get_diagnostic_name(macro_call.def_id) { Some(sym::assert_macro | sym::debug_assert_macro) => true, Some( sym::assert_eq_macro | sym::assert_ne_macro | sym::debug_assert_eq_macro | sym::debug_assert_ne_macro, ) => false, _ => return, }; // This lint would be very noisy in tests, so just ignore if we're in test context if is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id) { return; } let panic_expn = if single_argument { let Some((_, panic_expn)) = find_assert_args(cx, expr, macro_call.expn) else { return; }; panic_expn } else { let Some((_, _, panic_expn)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return; }; panic_expn }; if let PanicExpn::Empty = panic_expn { span_lint_and_help( cx, MISSING_ASSERT_MESSAGE, macro_call.span, "assert without any message", None, "consider describing why the failing assert is problematic", ); } } }