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    /// TCC threshold: Tight Class Cohesion (Lanza-Marinescu's 1/3)
24    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
112/// Tight Class Cohesion: ratio of method pairs sharing at least one field.
113fn 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}