Skip to main content

wowlab_tidy/languages/rust/rules/api/
ctor_param_count.rs

1use ra_ap_syntax::{ast, ast::HasName};
2
3use super::support::type_name;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "constructor with four parameters",
10        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(account: Account, amount: Currency, memo: Memo, clock: Clock) -> Self {\n        Deposit\n    }\n}",
11        pass: false,
12    },
13    Example {
14        label: "three consecutive str parameters",
15        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(bank: &str, customer: &str, currency: &str) -> Self {\n        Deposit\n    }\n}",
16        pass: false,
17    },
18    Example {
19        label: "with_ constructor returning type name",
20        code: "struct Deposit;\nimpl Deposit {\n    pub fn with_parts(a: Account, b: Currency, c: Memo, d: Clock) -> Deposit {\n        Deposit\n    }\n}",
21        pass: false,
22    },
23    Example {
24        label: "constructor with two parameters",
25        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(account: Account, amount: Currency) -> Self {\n        Deposit\n    }\n}",
26        pass: true,
27    },
28    Example {
29        label: "mixed types below threshold",
30        code: "struct Deposit;\nimpl Deposit {\n    pub fn new(amount: u64, bank: &str, id: u32) -> Self {\n        Deposit\n    }\n}",
31        pass: true,
32    },
33    Example {
34        label: "non-constructor name",
35        code: "struct Acc;\nimpl Acc {\n    fn combine(a: u32, b: u32, c: u32, d: u32) -> u32 {\n        a + b + c + d\n    }\n}",
36        pass: true,
37    },
38    Example {
39        label: "chainable with_ method has receiver",
40        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}",
41        pass: true,
42    },
43    Example {
44        label: "trait impl constructor",
45        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}",
46        pass: true,
47    },
48    Example {
49        label: "wide constructor in test module",
50        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}",
51        pass: true,
52    },
53];
54
55const MIN_SAME_TYPE_RUN: usize = 3;
56
57const PRIMITIVES: &[&str] = &[
58    "bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32",
59    "u64", "u128", "usize",
60];
61
62crate::ast_rule!(
63    ctor_param_count,
64    "Flag constructors with too many parameters or runs of identically-typed primitives — cascade construction through helper types.",
65    "Long or same-typed constructor parameter lists invite silent argument mix-ups; grouping parameters semantically makes construction self-checking.",
66    Medium,
67    params { threshold: i64 = 4 },
68);
69
70fn check_ctor_param_count(ctx: &AstCtx<'_>) -> Vec<Violation> {
71    let threshold = ctx
72        .file
73        .config
74        .get_usize("rust_ctor_param_count", &PARAMS[0]);
75
76    ctx.nodes::<ast::Impl>()
77        .filter(|item| !ctx.is_in_test(item) && item.trait_().is_none())
78        .flat_map(|item| {
79            let Some(ty) = item.self_ty().and_then(|ty| type_name(&ty)) else {
80                return Vec::new();
81            };
82
83            let associated_items = item
84                .assoc_item_list()
85                .into_iter()
86                .flat_map(|list| list.assoc_items());
87            let constructors = associated_items
88                .filter_map(|assoc| match assoc {
89                    ast::AssocItem::Fn(function) => Some(function),
90                    _ => None,
91                })
92                .filter(|function| is_ctor(function, &ty));
93
94            constructors
95                .flat_map(|function| ctor_violations(ctx, &function, &ty, threshold))
96                .collect::<Vec<_>>()
97        })
98        .collect()
99}
100
101fn is_ctor(function: &ast::Fn, ty: &str) -> bool {
102    if function
103        .param_list()
104        .is_some_and(|params| params.self_param().is_some())
105    {
106        return false;
107    }
108
109    let Some(name) = function.name().map(|name| name.text().to_string()) else {
110        return false;
111    };
112    let ctor_name = name == "new" || name.starts_with("new_") || name.starts_with("with_");
113
114    let return_type = function
115        .ret_type()
116        .and_then(|ret| ret.ty())
117        .and_then(|ty| type_name(&ty));
118
119    ctor_name && return_type.is_some_and(|name| name == "Self" || name == ty)
120}
121
122fn longest_primitive_run(function: &ast::Fn) -> Option<(&'static str, usize)> {
123    let mut best: Option<(&'static str, usize)> = None;
124    let mut current: Option<(&'static str, usize)> = None;
125
126    for param in function
127        .param_list()
128        .into_iter()
129        .flat_map(|params| params.params())
130    {
131        let key = param.ty().and_then(|ty| primitive_key(&ty));
132
133        current = match (current, key) {
134            (Some((run, len)), Some(next)) if run == next => Some((run, len + 1)),
135            (_, Some(next)) => Some((next, 1)),
136            (_, None) => None,
137        };
138
139        if let Some((run, len)) = current {
140            if best.is_none_or(|(_, best_len)| len > best_len) {
141                best = Some((run, len));
142            }
143        }
144    }
145
146    best
147}
148
149fn primitive_key(ty: &ast::Type) -> Option<&'static str> {
150    if let ast::Type::RefType(reference) = ty {
151        if reference.ty().and_then(|ty| type_name(&ty)).as_deref() == Some("str") {
152            return Some("&str");
153        }
154
155        return None;
156    }
157
158    let name = type_name(ty)?;
159
160    PRIMITIVES
161        .iter()
162        .copied()
163        .find(|primitive| *primitive == name)
164}
165
166fn ctor_violations(
167    ctx: &AstCtx<'_>,
168    function: &ast::Fn,
169    ty: &str,
170    threshold: usize,
171) -> Vec<Violation> {
172    let Some(name) = function.name() else {
173        return Vec::new();
174    };
175    let count = function
176        .param_list()
177        .map_or(0, |params| params.params().count());
178    let mut out = Vec::new();
179
180    if count >= threshold {
181        out.push(ctx.violation(&name, format!(
182            "constructor `{ty}::{name}` takes {count} parameters (max {}) — group them semantically via helper types", threshold - 1
183        )));
184    }
185
186    if let Some((run_ty, run_len)) = longest_primitive_run(function)
187        && run_len >= MIN_SAME_TYPE_RUN
188    {
189        out.push(ctx.violation(&name, format!(
190            "constructor `{ty}::{name}` has {run_len} consecutive `{run_ty}` parameters — mix-up risk, use distinct types"
191        )));
192    }
193
194    out
195}
196
197crate::tidy_ast_test!(check_ctor_param_count, {
198    crate::example_tests!(EXAMPLES, check_ctor_param_count);
199});