wowlab_tidy/languages/rust/rules/hygiene/
expect_message.rs1use ra_ap_syntax::ast::{self, HasArgList, LiteralKind};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "empty expect message",
9 code: "fn f() { Some(1).expect(\"\"); }",
10 pass: false,
11 },
12 Example {
13 label: "generic expect message",
14 code: "fn f() { Some(1).expect(\"failed\"); }",
15 pass: false,
16 },
17 Example {
18 label: "good expect message",
19 code: "fn f() { Some(1).expect(\"config file must exist at /etc/app.conf\"); }",
20 pass: true,
21 },
22 Example {
23 label: "expect in test module",
24 code: "#[cfg(test)]\nmod tests {\n fn t() { Some(1).expect(\"\"); }\n}",
25 pass: true,
26 },
27];
28
29crate::ast_rule!(
30 expect_message,
31 "Require `.expect()` to have a meaningful message, not generic ones.",
32 "Generic expect messages like 'failed' give no context in panics. Describe what was expected and why.",
33);
34
35const GENERIC_MESSAGES: &[&str] = &[
36 "",
37 "failed",
38 "error",
39 "unwrap",
40 "should not happen",
41 "unreachable",
42 "impossible",
43 "bug",
44];
45
46fn check_expect_message(ctx: &AstCtx<'_>) -> Vec<Violation> {
47 ctx.nodes::<ast::MethodCallExpr>()
48 .filter(|call| !ctx.is_in_test(call))
49 .filter_map(|call| {
50 let method = call.name_ref()?;
51
52 if method.text() != "expect" {
53 return None;
54 }
55
56 let ast::Expr::Literal(literal) = call.arg_list()?.args().next()? else {
57 return None;
58 };
59 let LiteralKind::String(text) = literal.kind() else {
60 return None;
61 };
62 let message = text.value().ok()?.into_owned();
63 let lower = message.trim().to_lowercase();
64
65 GENERIC_MESSAGES
66 .iter()
67 .any(|generic| lower == *generic)
68 .then(|| {
69 ctx.violation(
70 &method,
71 format!(
72 ".expect() with generic message {message:?} (provide a specific reason)"
73 ),
74 )
75 })
76 })
77 .collect()
78}
79
80crate::tidy_ast_test!(check_expect_message, {
81 crate::example_tests!(EXAMPLES, check_expect_message);
82});