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 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 let base = base.split('<').next().unwrap_or(base);
76 PRIMITIVE_TYPES.contains(&base)
77}