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