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