Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
panic_message.rs

1use ra_ap_syntax::{AstNode, AstToken, ast};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "unreachable without message",
9        code: "fn f() { unreachable!(); }",
10        pass: false,
11    },
12    Example {
13        label: "debug_assert without message",
14        code: "fn f(x: u8) { debug_assert!(x > 0); }",
15        pass: false,
16    },
17    Example {
18        label: "debug_assert_eq without message",
19        code: "fn f(a: u8, b: u8) { debug_assert_eq!(a, b); }",
20        pass: false,
21    },
22    Example {
23        label: "debug_assert_ne without message",
24        code: "fn f(a: u8, b: u8) { debug_assert_ne!(a, b); }",
25        pass: false,
26    },
27    Example {
28        label: "unreachable with empty message",
29        code: "fn f() { unreachable!(\"\"); }",
30        pass: false,
31    },
32    Example {
33        label: "unreachable with descriptive message",
34        code: "fn f(m: u8) { unreachable!(\"month {m} out of range after validation\"); }",
35        pass: true,
36    },
37    Example {
38        label: "unreachable with short message",
39        code: "fn f() { unreachable!(\"failed to seed rng before first roll\"); }",
40        pass: true,
41    },
42    Example {
43        label: "debug_assert with message",
44        code: "fn f(x: u8) { debug_assert!(x > 0, \"x must be positive, got {x}\"); }",
45        pass: true,
46    },
47    Example {
48        label: "debug_assert_eq with message",
49        code: "fn f(a: u8, b: u8) { debug_assert_eq!(a, b, \"lengths must match\"); }",
50        pass: true,
51    },
52    Example {
53        label: "plain assert not covered",
54        code: "fn f(x: u8) { assert!(x > 0); }",
55        pass: true,
56    },
57    Example {
58        label: "unreachable in test module",
59        code: "#[cfg(test)]\nmod tests {\n    fn t() { unreachable!(); }\n}",
60        pass: true,
61    },
62];
63
64crate::ast_rule!(
65    panic_message,
66    "Require a message on `unreachable!` and `debug_assert!*`.",
67    "Panic messages must state what went wrong; a missing or empty message gives the developer nothing to act on.",
68    Medium,
69);
70
71const COMPARISON_ASSERT_ARGUMENTS: usize = 2;
72
73fn check_panic_message(ctx: &AstCtx<'_>) -> Vec<Violation> {
74    ctx.nodes::<ast::MacroCall>()
75        .filter(|call| !ctx.is_in_test(call))
76        .filter_map(|call| {
77            let name = macro_name(&call)?;
78            let is_unreachable = name == "unreachable";
79            let is_debug_assert = name == "debug_assert";
80            let is_debug_assert_cmp = matches!(name.as_str(), "debug_assert_eq" | "debug_assert_ne");
81            let message = if is_unreachable && macro_tokens(&call).next().is_none() {
82                Some("unreachable!() without a message — state why this branch is impossible".to_owned())
83            } else if is_debug_assert || is_debug_assert_cmp {
84                let needed = if is_debug_assert {
85                    1
86                } else {
87                    COMPARISON_ASSERT_ARGUMENTS
88                };
89
90                (message_comma_count(&call) < needed).then(|| {
91                    "debug_assert without a message — describe the violated invariant".to_owned()
92                })
93            } else if is_unreachable {
94                first_string_literal(&call).and_then(|message| {
95                    is_empty_message(&message).then(|| {
96                        format!(
97                            "unreachable! message {message:?} is empty — state why this branch is impossible"
98                        )
99                    })
100                })
101            } else {
102                None
103            }?;
104
105            Some(ctx.violation(&call, message))
106        })
107        .collect()
108}
109
110fn message_comma_count(call: &ast::MacroCall) -> usize {
111    let tokens: Vec<_> = macro_tokens(call).collect();
112    let count = tokens.iter().filter(|token| token.text() == ",").count();
113
114    if tokens.last().is_some_and(|token| token.text() == ",") {
115        count - 1
116    } else {
117        count
118    }
119}
120
121fn first_string_literal(call: &ast::MacroCall) -> Option<String> {
122    ast::String::cast(macro_tokens(call).next()?)?
123        .value()
124        .ok()
125        .map(std::borrow::Cow::into_owned)
126}
127
128fn is_empty_message(msg: &str) -> bool {
129    msg.trim().is_empty()
130}
131
132fn macro_name(call: &ast::MacroCall) -> Option<String> {
133    call.path()?
134        .segment()?
135        .name_ref()
136        .map(|name| name.text().to_string())
137}
138
139fn macro_tokens(call: &ast::MacroCall) -> impl Iterator<Item = ra_ap_syntax::SyntaxToken> + '_ {
140    let tokens = call
141        .token_tree()
142        .into_iter()
143        .flat_map(|tree| tree.syntax().children_with_tokens())
144        .filter_map(ra_ap_syntax::NodeOrToken::into_token);
145
146    tokens
147        .filter(|token| !token.kind().is_trivia())
148        .filter(|token| !matches!(token.text(), "(" | ")" | "[" | "]" | "{" | "}"))
149}
150
151crate::tidy_ast_test!(check_panic_message, {
152    crate::example_tests!(EXAMPLES, check_panic_message);
153});