rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "constructor with four parameters",
        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(account: Account, amount: Currency, memo: Memo, clock: Clock) -> Self {\n        Deposit\n    }\n}",
        pass: false,
    },
    Example {
        label: "three consecutive str parameters",
        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(bank: &str, customer: &str, currency: &str) -> Self {\n        Deposit\n    }\n}",
        pass: false,
    },
    Example {
        label: "with_ constructor returning type name",
        code: "struct Deposit;\nimpl Deposit {\n    pub fn with_parts(a: Account, b: Currency, c: Memo, d: Clock) -> Deposit {\n        Deposit\n    }\n}",
        pass: false,
    },
    Example {
        label: "constructor with two parameters",
        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(account: Account, amount: Currency) -> Self {\n        Deposit\n    }\n}",
        pass: true,
    },
    Example {
        label: "mixed types below threshold",
        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(amount: u64, bank: &str, id: u32) -> Self {\n        Deposit\n    }\n}",
        pass: true,
    },
    Example {
        label: "non-constructor name",
        code: "struct Acc;\nimpl Acc {\n    fn combine(a: u32, b: u32, c: u32, d: u32) -> u32 {\n        a + b + c + d\n    }\n}",
        pass: true,
    },
    Example {
        label: "chainable with_ method has receiver",
        code: "struct Acc;\nimpl Acc {\n    pub fn with_size(mut self, a: u32, b: u32, c: u32, d: u32) -> Self {\n        self\n    }\n}",
        pass: true,
    },
    Example {
        label: "trait impl constructor",
        code: "struct Acc;\ntrait Make {\n    fn new_from(a: u32, b: u32, c: u32, d: u32) -> Self;\n}\nimpl Make for Acc {\n    fn new_from(a: u32, b: u32, c: u32, d: u32) -> Self {\n        Acc\n    }\n}",
        pass: true,
    },
    Example {
        label: "wide constructor in test module",
        code: "#[cfg(test)]\nmod tests {\n    struct Acc;\n    impl Acc {\n        pub fn new(a: u32, b: u32, c: u32, d: u32) -> Self {\n            Acc\n        }\n    }\n}",
        pass: true,
    },
];

const MIN_SAME_TYPE_RUN: usize = 3;

const PRIMITIVES: &[&str] = &[
    "bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32",
    "u64", "u128", "usize",
];

crate::ast_rule!(
    ctor_param_count,
    "Flag constructors with too many parameters or runs of identically-typed primitives — cascade construction through helper types.",
    "Long or same-typed constructor parameter lists invite silent argument mix-ups; grouping parameters semantically makes construction self-checking.",
    Medium,
    params { threshold: i64 = 4 },
);

fn check_ctor_param_count(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let threshold = ctx
        .file
        .config
        .get_usize("rust_ctor_param_count", &PARAMS[0]);

    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 constructors = associated_items
                .filter_map(|assoc| match assoc {
                    ast::AssocItem::Fn(function) => Some(function),
                    _ => None,
                })
                .filter(|function| is_ctor(function, &ty));

            constructors
                .flat_map(|function| ctor_violations(ctx, &function, &ty, threshold))
                .collect::<Vec<_>>()
        })
        .collect()
}

fn is_ctor(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;
    };
    let ctor_name = name == "new" || name.starts_with("new_") || name.starts_with("with_");

    let return_type = function
        .ret_type()
        .and_then(|ret| ret.ty())
        .and_then(|ty| type_name(&ty));

    ctor_name && return_type.is_some_and(|name| name == "Self" || name == ty)
}

fn longest_primitive_run(function: &ast::Fn) -> Option<(&'static str, usize)> {
    let mut best: Option<(&'static str, usize)> = None;
    let mut current: Option<(&'static str, usize)> = None;

    for param in function
        .param_list()
        .into_iter()
        .flat_map(|params| params.params())
    {
        let key = param.ty().and_then(|ty| primitive_key(&ty));

        current = match (current, key) {
            (Some((run, len)), Some(next)) if run == next => Some((run, len + 1)),
            (_, Some(next)) => Some((next, 1)),
            (_, None) => None,
        };

        if let Some((run, len)) = current
            && best.is_none_or(|(_, best_len)| len > best_len)
        {
            best = Some((run, len));
        }
    }

    best
}

fn primitive_key(ty: &ast::Type) -> Option<&'static str> {
    if let ast::Type::RefType(reference) = ty {
        if reference.ty().and_then(|ty| type_name(&ty)).as_deref() == Some("str") {
            return Some("&str");
        }

        return None;
    }

    let name = type_name(ty)?;

    PRIMITIVES
        .iter()
        .copied()
        .find(|primitive| *primitive == name)
}

fn ctor_violations(
    ctx: &AstCtx<'_>,
    function: &ast::Fn,
    ty: &str,
    threshold: usize,
) -> Vec<Violation> {
    let Some(name) = function.name() else {
        return Vec::new();
    };
    let count = function
        .param_list()
        .map_or(0, |params| params.params().count());
    let mut out = Vec::new();

    if count >= threshold {
        out.push(ctx.violation(&name, format!(
            "constructor `{ty}::{name}` takes {count} parameters (max {}) — group them semantically via helper types", threshold - 1
        )));
    }

    if let Some((run_ty, run_len)) = longest_primitive_run(function)
        && run_len >= MIN_SAME_TYPE_RUN
    {
        out.push(ctx.violation(&name, format!(
            "constructor `{ty}::{name}` has {run_len} consecutive `{run_ty}` parameters — mix-up risk, use distinct types"
        )));
    }

    out
}

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