Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
panic.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: "panic with meaningful message (bug detection)",
9        code: "fn f() { panic!(\"buffer len {} below header size\", 3); }",
10        pass: true,
11    },
12    Example {
13        label: "bare panic",
14        code: "fn f() { panic!(); }",
15        pass: false,
16    },
17    Example {
18        label: "empty panic message",
19        code: "fn f() { panic!(\"\"); }",
20        pass: false,
21    },
22    Example {
23        label: "unimplemented in production",
24        code: "fn f() { unimplemented!(); }",
25        pass: false,
26    },
27    Example {
28        label: "todo in production",
29        code: "fn f() { todo!(); }",
30        pass: false,
31    },
32    Example {
33        label: "panic in test module",
34        code: "#[cfg(test)]\nmod tests {\n    fn t() { panic!(); }\n}",
35        pass: true,
36    },
37    Example {
38        label: "no panic",
39        code: "fn f() -> Result<(), String> { Ok(()) }",
40        pass: true,
41    },
42    Example {
43        label: "expect is allowed",
44        code: "fn f() { Some(1).expect(\"always Some\"); }",
45        pass: true,
46    },
47];
48
49crate::ast_rule!(
50    panic,
51    "Ban `unimplemented!()`, `todo!()`, and message-less `panic!()` in library code.",
52    "Detected programming bugs must panic with a message (M-PANIC-ON-BUG, M-PANIC-MESSAGE); todo!/unimplemented! mark unfinished code and message-less panics help nobody.",
53    High,
54);
55
56fn check_panic(ctx: &AstCtx<'_>) -> Vec<Violation> {
57    ctx.nodes::<ast::MacroCall>()
58        .filter(|call| !ctx.is_in_test(call))
59        .filter_map(|call| {
60            let name = macro_name(&call)?;
61            let message = if matches!(name.as_str(), "unimplemented" | "todo") {
62                Some(format!(
63                    "{name}!() in production code (finish the implementation or return Result)"
64                ))
65            } else if name == "panic" {
66                panic_message_violation(&call).map(str::to_owned)
67            } else {
68                None
69            }?;
70
71            Some(ctx.violation(&call, message))
72        })
73        .collect()
74}
75
76fn macro_name(call: &ast::MacroCall) -> Option<String> {
77    call.path()?
78        .segment()?
79        .name_ref()
80        .map(|name| name.text().to_string())
81}
82
83fn panic_message_violation(call: &ast::MacroCall) -> Option<&'static str> {
84    let tree = call.token_tree()?;
85    let mut tokens = tree
86        .syntax()
87        .children_with_tokens()
88        .filter_map(ra_ap_syntax::NodeOrToken::into_token)
89        .filter(|token| !token.kind().is_trivia())
90        .filter(|token| !matches!(token.text(), "(" | ")" | "[" | "]" | "{" | "}"));
91    let Some(first) = tokens.next() else {
92        return Some("panic!() without a message (state what bug was detected)");
93    };
94
95    if ast::String::cast(first).is_some_and(|string| {
96        string
97            .value()
98            .is_ok_and(|message| message.trim().is_empty())
99    }) {
100        return Some("panic!(\"\") with an empty message (state what bug was detected)");
101    }
102
103    None
104}
105
106crate::tidy_ast_test!(check_panic, {
107    crate::example_tests!(EXAMPLES, check_panic);
108});