Skip to main content

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

1use ra_ap_syntax::ast;
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "full path catch_unwind",
9        code: "fn f() { let _ = std::panic::catch_unwind(|| {}); }",
10        pass: false,
11    },
12    Example {
13        label: "short path catch_unwind",
14        code: "fn f() { let _ = panic::catch_unwind(|| {}); }",
15        pass: false,
16    },
17    Example {
18        label: "method catch_unwind",
19        code: "fn f<F: Future>(fut: F) { let _ = fut.catch_unwind(); }",
20        pass: false,
21    },
22    Example {
23        label: "catch_unwind with boundary comment",
24        code: "fn f() {\n    // PANIC-BOUNDARY: isolates one request; the worker restarts after any unwind\n    let _ = std::panic::catch_unwind(|| {});\n}",
25        pass: true,
26    },
27    Example {
28        label: "catch_unwind in test module",
29        code: "#[cfg(test)]\nmod tests {\n    fn t() { let _ = std::panic::catch_unwind(|| {}); }\n}",
30        pass: true,
31    },
32    Example {
33        label: "no catch_unwind",
34        code: "fn f() -> Result<(), String> { Ok(()) }",
35        pass: true,
36    },
37];
38
39crate::ast_rule!(
40    catch_unwind,
41    "Require `// PANIC-BOUNDARY:` comment on `catch_unwind` calls.",
42    "Catching a panic and continuing risks observing broken state. The comment must state the controlled-restart story.",
43    High,
44);
45
46fn check_catch_unwind(ctx: &AstCtx<'_>) -> Vec<Violation> {
47    let calls = ctx
48        .nodes::<ast::CallExpr>()
49        .filter(|call| !ctx.is_in_test(call))
50        .filter_map(|call| {
51            let ast::Expr::PathExpr(path) = call.expr()? else {
52                return None;
53            };
54            let path = path.path()?;
55
56            path.segment()
57                .and_then(|segment| segment.name_ref())
58                .filter(|name| name.text() == "catch_unwind")
59        });
60    let methods = ctx
61        .nodes::<ast::MethodCallExpr>()
62        .filter(|call| !ctx.is_in_test(call))
63        .filter_map(|call| call.name_ref().filter(|name| name.text() == "catch_unwind"));
64
65    calls
66        .chain(methods)
67        .filter_map(|name| missing_boundary(ctx, &name))
68        .collect()
69}
70
71fn missing_boundary(ctx: &AstCtx<'_>, name: &ast::NameRef) -> Option<Violation> {
72    let line = ctx.line_of(name);
73
74    if !crate::infra::helpers::has_preceding_comment(ctx.file.lines, line, &["PANIC-BOUNDARY:"]) {
75        return Some(ctx.violation(
76            name,
77            "catch_unwind without // PANIC-BOUNDARY: comment stating the controlled-restart story",
78        ));
79    }
80
81    None
82}
83
84crate::tidy_ast_test!(check_catch_unwind, {
85    crate::example_tests!(EXAMPLES, check_catch_unwind);
86});