Skip to main content

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

1use 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
50// #t(fn: rust_clone_in_loop) inverted indexes own parameter signatures and member sets
51fn 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});