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