Skip to main content

cha_core/plugins/
primitive_obsession.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3const PRIMITIVE_TYPES: &[&str] = &[
4    "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", "f32",
5    "f64", "bool", "char", "String", "&str", "string", "number", "boolean", "any",
6];
7
8/// Detect functions where most parameters are primitive types.
9pub struct PrimitiveObsessionAnalyzer {
10    pub min_params: usize,
11    pub primitive_ratio: f64,
12}
13
14impl Default for PrimitiveObsessionAnalyzer {
15    fn default() -> Self {
16        Self {
17            min_params: 3,
18            primitive_ratio: 0.8,
19        }
20    }
21}
22
23impl Plugin for PrimitiveObsessionAnalyzer {
24    fn name(&self) -> &str {
25        "primitive_obsession"
26    }
27
28    fn description(&self) -> &str {
29        "Too many primitive parameter types"
30    }
31
32    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
33        ctx.model
34            .functions
35            .iter()
36            .filter_map(|f| {
37                let total = f.parameter_types.len();
38                if total < self.min_params {
39                    return None;
40                }
41                let prim_count = f.parameter_types.iter().filter(|t| is_primitive(t)).count();
42                let ratio = prim_count as f64 / total as f64;
43                if ratio < self.primitive_ratio {
44                    return None;
45                }
46                Some(Finding {
47                    smell_name: "primitive_obsession".into(),
48                    category: SmellCategory::Bloaters,
49                    severity: Severity::Hint,
50                    location: Location {
51                        path: ctx.file.path.clone(),
52                        start_line: f.start_line,
53                        end_line: f.end_line,
54                        name: Some(f.name.clone()),
55                    },
56                    message: format!(
57                        "Function `{}` uses mostly primitive parameter types",
58                        f.name
59                    ),
60                    suggested_refactorings: vec![
61                        "Replace Data Value with Object".into(),
62                        "Replace Type Code with Class".into(),
63                    ],
64                    actual_value: Some(ratio),
65                    threshold: Some(self.primitive_ratio),
66                })
67            })
68            .collect()
69    }
70}
71
72fn is_primitive(ty: &str) -> bool {
73    let base = ty.trim_start_matches('&').trim_start_matches("mut ").trim();
74    // Strip generic parameters: Vec<String> → Vec
75    let base = base.split('<').next().unwrap_or(base);
76    PRIMITIVE_TYPES.contains(&base)
77}