rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example { label: "manual display", code: "struct ParseError; impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } }", pass: false },
    Example { label: "manual error", code: "struct ParseError; impl std::error::Error for ParseError {}", pass: false },
    Example { label: "thiserror", code: "#[derive(Debug, thiserror::Error)] #[error(\"bad\")] struct ParseError;", pass: true },
    Example { label: "non-error display", code: "struct Label; impl std::fmt::Display for Label { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } }", pass: true },
    Example { label: "unrelated trait", code: "struct ParseError; impl Clone for ParseError { fn clone(&self) -> Self { Self } }", pass: true },
    Example { label: "test error", code: "#[cfg(test)] mod tests { struct StubError; impl std::error::Error for StubError {} }", pass: true },
];

crate::ast_rule!(
    manual_error_impl,
    "Reject hand-written `Display` and `Error` implementations for `*Error` types.",
    "Canonical errors derive `thiserror::Error` so formatting and source propagation remain declarative and consistent.",
    Low,
);

fn check_manual_error_impl(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::Impl>()
        .filter(|item_impl| !ctx.is_in_test(item_impl))
        .filter_map(|item_impl| {
            let self_path = item_impl
                .self_ty()
                .and_then(|ty| match ty {
                    ast::Type::PathType(path_type) => path_type.path(),
                    _ => None,
                })
                .and_then(|path| path.segment());
            let self_name = self_path
                .and_then(|segment| segment.name_ref())?;

            if !self_name.text().ends_with("Error") {
                return None;
            }

            let trait_type = item_impl.trait_()?;
            let normalized: String = trait_type
                .syntax()
                .text()
                .to_string()
                .chars()
                .filter(|ch| !ch.is_whitespace())
                .collect();
            let trait_name = match normalized.as_str() {
                "Display" | "fmt::Display" | "std::fmt::Display" => "Display",
                "Error" | "error::Error" | "std::error::Error" => "std::error::Error",
                _ => return None,
            };

            Some(ctx.violation(
                &item_impl,
                format!(
                    "manual `{trait_name}` implementation for `{self_name}` — derive `thiserror::Error`"
                ),
            ))
        })
        .collect()
}

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