wowlab_tidy/languages/rust/rules/api/
ctor_param_count.rs1use 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});