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 smells(&self) -> Vec<String> {
29        vec!["primitive_obsession".into()]
30    }
31
32    fn description(&self) -> &str {
33        "Too many primitive parameter types"
34    }
35
36    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
37        ctx.model
38            .functions
39            .iter()
40            .filter_map(|f| {
41                let total = f.parameter_types.len();
42                if total < self.min_params {
43                    return None;
44                }
45                let prim_count = f.parameter_types.iter().filter(|t| is_primitive(t)).count();
46                let ratio = prim_count as f64 / total as f64;
47                if ratio < self.primitive_ratio {
48                    return None;
49                }
50                Some(Finding {
51                    smell_name: "primitive_obsession".into(),
52                    category: SmellCategory::Bloaters,
53                    severity: Severity::Hint,
54                    location: Location {
55                        path: ctx.file.path.clone(),
56                        start_line: f.start_line,
57                        start_col: f.name_col,
58                        end_line: f.start_line,
59                        end_col: f.name_end_col,
60                        name: Some(f.name.clone()),
61                    },
62                    message: format!(
63                        "Function `{}` uses mostly primitive parameter types",
64                        f.name
65                    ),
66                    suggested_refactorings: vec![
67                        "Replace Data Value with Object".into(),
68                        "Replace Type Code with Class".into(),
69                    ],
70                    actual_value: Some(ratio),
71                    threshold: Some(self.primitive_ratio),
72                })
73            })
74            .collect()
75    }
76}
77
78fn is_primitive(ty: &str) -> bool {
79    let base = ty.trim_start_matches('&').trim_start_matches("mut ").trim();
80    // Strip generic parameters: Vec<String> → Vec
81    let base = base.split('<').next().unwrap_or(base);
82    PRIMITIVE_TYPES.contains(&base)
83}