cha_core/plugins/
primitive_obsession.rs1use 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
8pub 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 risk_score: None,
77 })
78 })
79 .collect()
80 }
81}
82
83fn is_primitive(ty: &str) -> bool {
84 let base = ty.trim_start_matches('&').trim_start_matches("mut ").trim();
85 let base = base.split('<').next().unwrap_or(base);
87 PRIMITIVE_TYPES.contains(&base)
88}