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