rulewright 0.1.2

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::{
    ast,
    ast::{HasGenericArgs, HasName},
};

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example { label: "owned string error", code: "fn parse() -> Result<u32, String> { Err(String::new()) }", pass: false },
    Example { label: "borrowed string error", code: "fn parse() -> Result<u32, &'static str> { Err(\"bad\") }", pass: false },
    Example { label: "qualified result", code: "fn parse() -> std::result::Result<u32, String> { Err(String::new()) }", pass: false },
    Example { label: "typed error", code: "fn parse() -> Result<u32, ParseError> { todo!() }", pass: true },
    Example { label: "string success", code: "fn parse() -> Result<String, ParseError> { todo!() }", pass: true },
    Example { label: "test helper", code: "#[cfg(test)] mod tests { fn parse() -> Result<u32, String> { Err(String::new()) } }", pass: true },
];

crate::ast_rule!(
    string_error,
    "Reject `String` and `&str` as function error types.",
    "Structured error types preserve context, support source chains, and give callers stable inspection APIs.",
    Medium,
);

fn check_string_error(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::Fn>()
        .filter(|function| !ctx.is_in_test(function))
        .filter_map(|function| {
            let return_type = function.ret_type()?.ty()?;

            string_error_type(&return_type).then(|| {
                let name = function
                    .name()
                    .map_or_else(|| "<anonymous>".to_owned(), |name| name.text().to_string());

                ctx.violation(
                    &return_type,
                    format!(
                        "function `{name}` returns an unstructured string error — use a canonical error type"
                    ),
                )
            })
        })
        .collect()
}

fn string_error_type(ty: &ast::Type) -> bool {
    let ast::Type::PathType(path_type) = ty else {
        return false;
    };
    let Some(segment) = path_type.path().and_then(|path| path.segment()) else {
        return false;
    };

    if segment
        .name_ref()
        .is_none_or(|name| name.text() != "Result")
    {
        return false;
    }

    let Some(error_type) = segment
        .generic_arg_list()
        .and_then(|args| args.generic_args().nth(1))
        .and_then(|arg| match arg {
            ast::GenericArg::TypeArg(arg) => arg.ty(),
            _ => None,
        })
    else {
        return false;
    };

    match error_type {
        ast::Type::PathType(path_type) => {
            let segment = path_type
                .path()
                .and_then(|path| path.segment())
                .and_then(|segment| segment.name_ref());

            segment.is_some_and(|name| name.text() == "String")
        }

        ast::Type::RefType(reference) => {
            let path = reference
                .ty()
                .and_then(|ty| match ty {
                    ast::Type::PathType(path_type) => path_type.path(),
                    _ => None,
                })
                .and_then(|path| path.segment());

            path.and_then(|segment| segment.name_ref())
                .is_some_and(|name| name.text() == "str")
        }

        _ => false,
    }
}

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