Skip to main content

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

1use ra_ap_syntax::{AstNode, ast};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "panic in Result fn",
9        code: r#"fn f() -> Result<(), String> { panic!("x"); }"#,
10        pass: false,
11    },
12    Example {
13        label: "panic in non-Result fn",
14        code: r#"fn f() -> i32 { panic!("x"); }"#,
15        pass: true,
16    },
17    Example {
18        label: "Result fn with Err",
19        code: r#"fn f() -> Result<(), String> { Err("x".into()) }"#,
20        pass: true,
21    },
22    Example {
23        label: "unwrap in Result fn",
24        code: "fn f() -> Result<(), String> { Some(1).unwrap(); Ok(()) }",
25        pass: false,
26    },
27    Example {
28        label: "expect in Result fn",
29        code: r#"fn f() -> Result<(), String> { Some(1).expect("msg"); Ok(()) }"#,
30        pass: false,
31    },
32    Example {
33        label: "panic in Result fn in test module",
34        code: "#[cfg(test)]\nmod tests {\n    fn f() -> Result<(), String> { panic!(\"x\"); }\n}",
35        pass: true,
36    },
37    Example {
38        label: "todo in Result fn",
39        code: "fn f() -> Result<(), String> { todo!(); }",
40        pass: false,
41    },
42];
43
44crate::ast_rule!(
45    panic_in_result_fn,
46    "Ban `panic!`, `.unwrap()`, `.expect()` in functions returning `Result`.",
47    "A function returning Result promises fallible error handling. Panicking inside it breaks that contract.",
48    High,
49);
50
51fn check_panic_in_result_fn(ctx: &AstCtx<'_>) -> Vec<Violation> {
52    let macros = ctx
53        .nodes::<ast::MacroCall>()
54        .filter(|call| !ctx.is_in_test(call) && inside_result_fn(call))
55        .filter_map(|call| {
56            let name = call.path()?.segment()?.name_ref()?.text().to_string();
57
58            PANIC_MACROS.contains(&name.as_str()).then(|| {
59                ctx.violation(
60                    &call,
61                    format!("{name}!() inside a function returning Result — use `?` or return Err"),
62                )
63            })
64        });
65    let methods = ctx
66        .nodes::<ast::MethodCallExpr>()
67        .filter(|call| !ctx.is_in_test(call) && inside_result_fn(call))
68        .filter_map(|call| {
69            let name = call.name_ref()?;
70
71            matches!(name.text().as_str(), "unwrap" | "expect").then(|| {
72                ctx.violation(
73                    &name,
74                    format!(
75                        ".{}() inside a function returning Result — use `?` or return Err",
76                        name.text()
77                    ),
78                )
79            })
80        });
81
82    macros.chain(methods).collect()
83}
84
85fn returns_result(function: &ast::Fn) -> bool {
86    let Some(ast::Type::PathType(path)) = function.ret_type().and_then(|ret| ret.ty()) else {
87        return false;
88    };
89
90    let name = path
91        .path()
92        .and_then(|path| path.segment())
93        .and_then(|segment| segment.name_ref());
94
95    name.is_some_and(|name| name.text() == "Result")
96}
97
98const PANIC_MACROS: &[&str] = &["panic", "todo", "unimplemented"];
99
100fn inside_result_fn<N>(node: &N) -> bool
101where
102    N: AstNode,
103{
104    node.syntax()
105        .ancestors()
106        .filter_map(ast::Fn::cast)
107        .any(|function| returns_result(&function))
108}
109
110crate::tidy_ast_test!(check_panic_in_result_fn, {
111    crate::example_tests!(EXAMPLES, check_panic_in_result_fn);
112});