wowlab_tidy/languages/rust/rules/correctness/
panic.rs1use 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});