wowlab_tidy/languages/rust/rules/api/
param_clump.rs1use wowlab_types::sim::FastMap;
2
3use crate::{
4 Example, Violation,
5 languages::workspace::{FunctionRecord, WorkspaceCtx},
6 violation,
7};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11 Example {
12 label: "three parameters travel across three functions",
13 code: "fn one(user: String, region: u32, locale: String) {}\nfn two(user: String, region: u32, locale: String, extra: bool) {}\nfn three(user: String, region: u32, locale: String) {}",
14 pass: false,
15 },
16 Example {
17 label: "pass through strengthens evidence",
18 code: "fn sink(user: String, region: u32, locale: String) {}\nfn middle(user: String, region: u32, locale: String) { sink(user, region, locale); }\nfn source(user: String, region: u32, locale: String) { middle(user, region, locale); }",
19 pass: false,
20 },
21 Example {
22 label: "trait impl methods are exempt",
23 code: "trait Handle { fn one(&self, user: String, region: u32, locale: String); fn two(&self, user: String, region: u32, locale: String); fn three(&self, user: String, region: u32, locale: String); }\nstruct Handler; impl Handle for Handler { fn one(&self, user: String, region: u32, locale: String) {} fn two(&self, user: String, region: u32, locale: String) {} fn three(&self, user: String, region: u32, locale: String) {} }",
24 pass: true,
25 },
26 Example {
27 label: "only two functions",
28 code: "fn one(user: String, region: u32, locale: String) {}\nfn two(user: String, region: u32, locale: String) {}",
29 pass: true,
30 },
31];
32
33crate::workspace_rule!(
34 param_clump,
35 "Find maximal parameter groups repeated across functions; full-workspace runs are authoritative.",
36 "Parameters that repeatedly travel together usually represent one missing domain value object.",
37 Low,
38 params {
39 min_clump: i64 = 3,
40 min_fns: i64 = 3
41 },
42);
43
44#[derive(Clone, Copy)]
45struct Located<'a> {
46 rel: &'a str,
47 record: &'a FunctionRecord,
48}
49
50fn check_param_clump(ctx: &WorkspaceCtx<'_>) -> Vec<Violation> {
52 let min_clump = ctx.config.get_usize("rust_param_clump", &PARAMS[0]);
53 let min_fns = ctx.config.get_usize("rust_param_clump", &PARAMS[1]);
54 let functions: Vec<Located<'_>> = ctx
55 .files
56 .iter()
57 .flat_map(|file| {
58 file.functions.iter().map(|record| Located {
59 rel: &file.rel,
60 record,
61 })
62 })
63 .collect();
64 let mut inverted = FastMap::<(String, String), Vec<usize>>::default();
65
66 for (index, function) in functions.iter().enumerate() {
67 for parameter in &function.record.params {
68 inverted.entry(parameter.clone()).or_default().push(index);
69 }
70 }
71
72 let mut clumps = FastMap::<Vec<usize>, Vec<(String, String)>>::default();
73
74 for (parameter, mut members) in inverted {
75 members.sort_unstable();
76 members.dedup();
77
78 if members.len() >= min_fns {
79 clumps.entry(members).or_default().push(parameter);
80 }
81 }
82
83 clumps
84 .into_iter()
85 .filter_map(|(members, mut parameters)| {
86 if parameters.len() < min_clump {
87 return None;
88 }
89
90 parameters.sort_unstable();
91 let mut located: Vec<Located<'_>> = members
92 .iter()
93 .filter_map(|index| functions.get(*index).copied())
94 .collect();
95
96 located.sort_by_key(|item| (item.rel, item.record.line));
97 let anchor = *located.last()?;
98 let fields = parameters
99 .iter()
100 .map(|(name, ty)| format!("{name}: {ty}"))
101 .collect::<Vec<_>>()
102 .join(", ");
103 let member_list = located
104 .iter()
105 .map(|item| {
106 format!("{}:{} ({})", item.rel, item.record.line, item.record.name)
107 })
108 .collect::<Vec<_>>()
109 .join(", ");
110 let names: Vec<&str> = parameters.iter().map(|(name, _)| name.as_str()).collect();
111 let pass_through = located
112 .iter()
113 .flat_map(|item| &item.record.pass_through_calls)
114 .filter(|args| {
115 names
116 .iter()
117 .all(|name| args.iter().any(|arg| arg == name))
118 })
119 .count();
120
121 Some(violation(
122 anchor.rel,
123 anchor.record.line,
124 format!(
125 "parameter clump [{fields}] appears in {member_list}; {pass_through} pass-through call(s) reinforce it"
126 ),
127 ))
128 })
129 .collect()
130}
131
132crate::tidy_workspace_test!(check_param_clump, {
133 crate::example_tests!(EXAMPLES, check_param_clump);
134});