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 end_line: c.end_line,
72 name: Some(c.name.clone()),
73 },
74 message: format!(
75 "Class `{}` is a God Class (ATFD={atfd}, WMC={wmc}, TCC={tcc:.2})",
76 c.name
77 ),
78 suggested_refactorings: vec![
79 "Extract Class".into(),
80 "Single Responsibility Principle".into(),
81 ],
82 ..Default::default()
83 })
84 }
85}
86
87fn class_methods<'a>(c: &ClassInfo, ctx: &'a AnalysisContext) -> Vec<&'a FunctionInfo> {
88 ctx.model
89 .functions
90 .iter()
91 .filter(|f| f.start_line >= c.start_line && f.end_line <= c.end_line)
92 .collect()
93}
94
95fn count_atfd(methods: &[&FunctionInfo]) -> usize {
96 let ext: HashSet<&str> = methods
97 .iter()
98 .flat_map(|f| f.external_refs.iter().map(|s| s.as_str()))
99 .collect();
100 ext.len()
101}
102
103fn compute_tcc(methods: &[&FunctionInfo]) -> f64 {
105 let sets: Vec<HashSet<&str>> = methods
106 .iter()
107 .map(|f| f.referenced_fields.iter().map(|s| s.as_str()).collect())
108 .filter(|s: &HashSet<&str>| !s.is_empty())
109 .collect();
110 if sets.len() < 2 {
111 return 1.0;
112 }
113 let mut total = 0usize;
114 let mut shared = 0usize;
115 for i in 0..sets.len() {
116 for j in (i + 1)..sets.len() {
117 total += 1;
118 if sets[i].intersection(&sets[j]).next().is_some() {
119 shared += 1;
120 }
121 }
122 }
123 if total == 0 {
124 1.0
125 } else {
126 shared as f64 / total as f64
127 }
128}