rulewright 0.1.2

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::ast;

use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "full path catch_unwind",
        code: "fn f() { let _ = std::panic::catch_unwind(|| {}); }",
        pass: false,
    },
    Example {
        label: "short path catch_unwind",
        code: "fn f() { let _ = panic::catch_unwind(|| {}); }",
        pass: false,
    },
    Example {
        label: "method catch_unwind",
        code: "fn f<F: Future>(fut: F) { let _ = fut.catch_unwind(); }",
        pass: false,
    },
    Example {
        label: "catch_unwind with boundary comment",
        code: "fn f() {\n    // PANIC-BOUNDARY: isolates one request; the worker restarts after any unwind\n    let _ = std::panic::catch_unwind(|| {});\n}",
        pass: true,
    },
    Example {
        label: "catch_unwind in test module",
        code: "#[cfg(test)]\nmod tests {\n    fn t() { let _ = std::panic::catch_unwind(|| {}); }\n}",
        pass: true,
    },
    Example {
        label: "no catch_unwind",
        code: "fn f() -> Result<(), String> { Ok(()) }",
        pass: true,
    },
];

crate::ast_rule!(
    catch_unwind,
    "Require `// PANIC-BOUNDARY:` comment on `catch_unwind` calls.",
    "Catching a panic and continuing may expose poisoned or partially mutated state. Prefer an ordinary Result boundary; when isolating plugins, callbacks, or worker tasks, document what state is discarded and where execution restarts safely.",
    High,
);

fn check_catch_unwind(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let calls = ctx
        .nodes::<ast::CallExpr>()
        .filter(|call| !ctx.is_in_test(call))
        .filter_map(|call| {
            let ast::Expr::PathExpr(path) = call.expr()? else {
                return None;
            };
            let path = path.path()?;

            path.segment()
                .and_then(|segment| segment.name_ref())
                .filter(|name| name.text() == "catch_unwind")
        });
    let methods = ctx
        .nodes::<ast::MethodCallExpr>()
        .filter(|call| !ctx.is_in_test(call))
        .filter_map(|call| call.name_ref().filter(|name| name.text() == "catch_unwind"));

    calls
        .chain(methods)
        .filter_map(|name| missing_boundary(ctx, &name))
        .collect()
}

fn missing_boundary(ctx: &AstCtx<'_>, name: &ast::NameRef) -> Option<Violation> {
    let line = ctx.line_of(name);

    if !crate::infra::helpers::has_preceding_comment(ctx.file.lines, line, &["PANIC-BOUNDARY:"]) {
        return Some(ctx.violation(
            name,
            "catch_unwind without // PANIC-BOUNDARY: comment stating the controlled-restart story",
        ));
    }

    None
}

crate::rulewright_ast_test!(check_catch_unwind, {
    crate::example_tests!(EXAMPLES, check_catch_unwind);
});