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                        start_col: f.name_col,
54                        end_line: f.start_line,
55                        end_col: f.name_end_col,
56                        name: Some(f.name.clone()),
57                    },
58                    message: format!(
59                        "Function `{}` uses mostly primitive parameter types",
60                        f.name
61                    ),
62                    suggested_refactorings: vec![
63                        "Replace Data Value with Object".into(),
64                        "Replace Type Code with Class".into(),
65                    ],
66                    actual_value: Some(ratio),
67                    threshold: Some(self.primitive_ratio),
68                })
69            })
70            .collect()
71    }
72}
73
74fn is_primitive(ty: &str) -> bool {
75    let base = ty.trim_start_matches('&').trim_start_matches("mut ").trim();
76    // Strip generic parameters: Vec<String> → Vec
77    let base = base.split('<').next().unwrap_or(base);
78    PRIMITIVE_TYPES.contains(&base)
79}