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