about summary refs log tree commit diff
path: root/clippy_lints/src/missing_assert_message.rs
blob: 04df7b7a7e5a4c4b8ce7f110cf9928536b5db8f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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",
            );
        }
    }
}