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