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                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
103/// Tight Class Cohesion: ratio of method pairs sharing at least one field.
104fn 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}