rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "builder parameter",
        code: "fn make(b: WidgetBuilder) {}",
        pass: false,
    },
    Example {
        label: "factory reference parameter",
        code: "fn make(f: &WidgetFactory) {}",
        pass: false,
    },
    Example {
        label: "builder parameter on method",
        code: "struct App;\nimpl App {\n    fn install(&self, b: WidgetBuilder) {}\n}",
        pass: false,
    },
    Example {
        label: "closure factory parameter",
        code: "fn make(f: impl Fn() -> Widget) {}",
        pass: true,
    },
    Example {
        label: "plain parameter",
        code: "fn make(w: Widget) {}",
        pass: true,
    },
    Example {
        label: "builder parameter in test module",
        code: "#[cfg(test)]\nmod tests {\n    fn make(b: WidgetBuilder) {}\n}",
        pass: true,
    },
];

crate::ast_rule!(
    builder_param,
    "Flag parameters typed `*Builder`/`*Factory` — ask for `impl Fn() -> T` instead.",
    "Accepting factories or builders as parameters imports OO indirection; an impl Fn() -> T expresses repeatable instantiation idiomatically.",
    Low,
);

fn check_builder_param(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::Fn>()
        .filter(|function| !ctx.is_in_test(function))
        .flat_map(|function| {
            function
                .param_list()
                .into_iter()
                .flat_map(|params| params.params())
                .filter_map(|param| {
                    let ty = param.ty()?;
                    let ident = weasel_param_ident(&ty)?;

                    Some(ctx.violation(
                        &ty,
                        format!(
                            "parameter of type `{ident}` — accept `impl Fn() -> T` instead of a builder/factory"
                        ),
                    ))
                })
                .collect::<Vec<_>>()
        })
        .collect()
}

fn weasel_param_ident(ty: &ast::Type) -> Option<String> {
    let inner = match ty {
        ast::Type::RefType(reference) => reference.ty()?,
        _ => ty.clone(),
    };
    let ast::Type::PathType(path) = inner else {
        return None;
    };
    let name = path.path()?.segment()?.name_ref()?.text().to_string();

    (name.ends_with("Builder") || name.ends_with("Factory")).then_some(name)
}

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