cha_core/plugins/
god_class.rs1use std::collections::HashSet;
2
3use crate::{
4 AnalysisContext, ClassInfo, Finding, FunctionInfo, Location, Plugin, Severity, SmellCategory,
5};
6
7pub struct GodClassAnalyzer {
19 pub max_external_refs: usize,
21 pub min_wmc: usize,
23}
24
25impl Default for GodClassAnalyzer {
26 fn default() -> Self {
27 Self {
28 max_external_refs: 5,
29 min_wmc: 47,
30 }
31 }
32}
33
34impl Plugin for GodClassAnalyzer {
35 fn name(&self) -> &str {
36 "god_class"
37 }
38
39 fn description(&self) -> &str {
40 "God Class: high coupling, low cohesion"
41 }
42
43 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
44 ctx.model
45 .classes
46 .iter()
47 .filter_map(|c| self.check(c, ctx))
48 .collect()
49 }
50}
51
52impl GodClassAnalyzer {
53 fn check(&self, c: &ClassInfo, ctx: &AnalysisContext) -> Option<Finding> {
54 let methods = class_methods(c, ctx);
55 if methods.is_empty() {
56 return None;
57 }
58 let atfd = count_atfd(&methods);
59 let wmc: usize = methods.iter().map(|f| f.complexity).sum();
60 let tcc = compute_tcc(&methods);
61 if atfd <= self.max_external_refs || wmc < self.min_wmc || tcc >= 0.33 {
62 return None;
63 }
64 Some(Finding {
65 smell_name: "god_class".into(),
66 category: SmellCategory::Bloaters,
67 severity: Severity::Warning,
68 location: Location {
69 path: ctx.file.path.clone(),
70 start_line: c.start_line,
71 start_col: c.name_col,
72 end_line: c.start_line,
73 end_col: c.name_end_col,
74 name: Some(c.name.clone()),
75 },
76 message: format!(
77 "Class `{}` is a God Class (ATFD={atfd}, WMC={wmc}, TCC={tcc:.2})",
78 c.name
79 ),
80 suggested_refactorings: vec![
81 "Extract Class".into(),
82 "Single Responsibility Principle".into(),
83 ],
84 ..Default::default()
85 })
86 }
87}
88
89fn class_methods<'a>(c: &ClassInfo, ctx: &'a AnalysisContext) -> Vec<&'a FunctionInfo> {
90 ctx.model
91 .functions
92 .iter()
93 .filter(|f| f.start_line >= c.start_line && f.end_line <= c.end_line)
94 .collect()
95}
96
97fn count_atfd(methods: &[&FunctionInfo]) -> usize {
98 let ext: HashSet<&str> = methods
99 .iter()
100 .flat_map(|f| f.external_refs.iter().map(|s| s.as_str()))
101 .collect();
102 ext.len()
103}
104
105fn compute_tcc(methods: &[&FunctionInfo]) -> f64 {
107 let sets: Vec<HashSet<&str>> = methods
108 .iter()
109 .map(|f| f.referenced_fields.iter().map(|s| s.as_str()).collect())
110 .filter(|s: &HashSet<&str>| !s.is_empty())
111 .collect();
112 if sets.len() < 2 {
113 return 1.0;
114 }
115 let mut total = 0usize;
116 let mut shared = 0usize;
117 for i in 0..sets.len() {
118 for j in (i + 1)..sets.len() {
119 total += 1;
120 if sets[i].intersection(&sets[j]).next().is_some() {
121 shared += 1;
122 }
123 }
124 }
125 if total == 0 {
126 1.0
127 } else {
128 shared as f64 / total as f64
129 }
130}