Skip to main content

cha_core/plugins/
god_class.rs

1use std::collections::HashSet;
2
3use crate::{
4    AnalysisContext, ClassInfo, Finding, FunctionInfo, Location, Plugin, Severity, SmellCategory,
5};
6
7/// Detect God Classes using the detection strategy from [1]:
8///
9///   (ATFD > Few) AND (WMC >= VeryHigh) AND (TCC < 1/3)
10///
11/// ## References
12///
13/// [1] M. Lanza and R. Marinescu, "Object-Oriented Metrics in Practice:
14///     Using Software Metrics to Characterize, Evaluate, and Improve the
15///     Design of Object-Oriented Systems," Springer, 2006.
16///     doi: 10.1007/3-540-39538-5. Chapter 6.1, pp. 79–83.
17///     Thresholds derived from Table A.2 (45 Java projects).
18pub struct GodClassAnalyzer {
19    /// ATFD threshold: Access to Foreign Data (Few = 5)
20    pub max_external_refs: usize,
21    /// WMC threshold: Weighted Method Count (VeryHigh = 47)
22    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
105/// Tight Class Cohesion: ratio of method pairs sharing at least one field.
106fn 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}