rulewright 0.1.1

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

use super::support::type_name;
use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "unrelated helper in impl",
        code: "struct Db;\nimpl Db {\n    fn check_parameters(input: &str) -> bool {\n        input.is_empty()\n    }\n}",
        pass: false,
    },
    Example {
        label: "constructor",
        code: "struct Db;\nimpl Db {\n    fn new() -> Self {\n        Db\n    }\n}",
        pass: true,
    },
    Example {
        label: "returns the impl type",
        code: "struct Db;\nimpl Db {\n    fn connect(url: &str) -> Db {\n        Db\n    }\n}",
        pass: true,
    },
    Example {
        label: "takes Self parameters",
        code: "struct Db;\nimpl Db {\n    fn merge(a: Self, b: Self) -> u32 {\n        0\n    }\n}",
        pass: true,
    },
    Example {
        label: "method with receiver",
        code: "struct Db;\nimpl Db {\n    fn query(&self) {}\n}",
        pass: true,
    },
    Example {
        label: "from_ prefixed",
        code: "struct Db;\nimpl Db {\n    fn from_code(code: u32) -> u32 {\n        code\n    }\n}",
        pass: true,
    },
    Example {
        label: "parameter type mentioning impl type",
        code: "struct Config;\nimpl Config {\n    fn validate_rule(rule: &RuleConfig) -> bool {\n        true\n    }\n}",
        pass: true,
    },
    Example {
        label: "trait impl",
        code: "struct S;\ntrait Calc {\n    fn calc(x: u32) -> u32;\n}\nimpl Calc for S {\n    fn calc(x: u32) -> u32 {\n        x\n    }\n}",
        pass: true,
    },
    Example {
        label: "unrelated helper in test module",
        code: "#[cfg(test)]\nmod tests {\n    struct Db;\n    impl Db {\n        fn check_parameters(input: &str) -> bool {\n            input.is_empty()\n        }\n    }\n}",
        pass: true,
    },
];

const EXEMPT_NAMES: &[&str] = &["new", "default", "builder"];
const EXEMPT_PREFIXES: &[&str] = &["from_", "with_", "try_", "new_"];

crate::ast_rule!(
    assoc_fn_no_self,
    "Flag inherent associated fns that neither take nor return the impl type — make them free functions.",
    "Regular functions are first-class in Rust; computation unrelated to a receiver hosted in an impl block adds Type:: noise for no benefit.",
    Low,
);

fn check_assoc_fn_no_self(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::Impl>()
        .filter(|item| !ctx.is_in_test(item) && item.trait_().is_none())
        .flat_map(|item| {
            let Some(ty) = item.self_ty().and_then(|ty| type_name(&ty)) else {
                return Vec::new();
            };

            let associated_items = item
                .assoc_item_list()
                .into_iter()
                .flat_map(|list| list.assoc_items());
            let unrelated_functions = associated_items
                .filter_map(|assoc| match assoc { ast::AssocItem::Fn(function) => Some(function), _ => None })
                .filter(|function| is_unrelated_assoc_fn(function, &ty));

            unrelated_functions
                .filter_map(|function| {
                    let name = function.name()?;

                    Some(ctx.violation(
                        &name,
                        format!(
                            "associated fn `{ty}::{name}` neither takes nor returns `{ty}` — make it a free function"
                        ),
                    ))
                })
                .collect::<Vec<_>>()
        })
        .collect()
}

fn is_unrelated_assoc_fn(function: &ast::Fn, ty: &str) -> bool {
    if function
        .param_list()
        .is_some_and(|params| params.self_param().is_some())
    {
        return false;
    }

    let Some(name) = function.name().map(|name| name.text().to_string()) else {
        return false;
    };

    if EXEMPT_NAMES.contains(&name.as_str())
        || EXEMPT_PREFIXES
            .iter()
            .any(|prefix| name.starts_with(prefix))
    {
        return false;
    }

    !signature_mentions_type(function, ty)
}

fn signature_mentions_type(function: &ast::Fn, ty: &str) -> bool {
    let inputs = function
        .param_list()
        .map_or_else(String::new, |params| params.syntax().text().to_string());

    if inputs.contains("Self") || inputs.contains(ty) {
        return true;
    }

    function.ret_type().is_some_and(|ret| {
        let text = ret.syntax().text().to_string();

        text.contains("Self") || text.contains(ty)
    })
}

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