use crate::detectors::base::{Detector, DetectorConfig};
use crate::detectors::class_context::{ClassContextBuilder, ClassContextMap, ClassRole};
use crate::graph::GraphQueryExt;
use crate::models::{Finding, LineRange, Severity};
use anyhow::Result;
use regex::Regex;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct GodClassThresholds {
pub max_methods: usize,
pub critical_methods: usize,
pub max_lines: usize,
pub critical_lines: usize,
pub max_complexity: usize,
pub critical_complexity: usize,
}
impl Default for GodClassThresholds {
fn default() -> Self {
Self {
max_methods: 20,
critical_methods: 30,
max_lines: 500,
critical_lines: 1000,
max_complexity: 100,
critical_complexity: 200,
}
}
}
const EXCLUDED_PATTERNS: &[&str] = &[
r".*Client$", r".*Connection$", r".*Session$", r".*Pipeline$", r".*Engine$", r".*Generator$", r".*Builder$", r".*Factory$", r".*Manager$", r".*Controller$", r".*Adapter$", r".*Facade$", r".*Handler$", r".*Dispatcher$", r".*Orchestrator$", r".*Coordinator$", r".*Router$", r".*Middleware$", r".*Resolver$", r".*Presenter$", r".*Mediator$", r".*ViewSet$", ];
pub struct GodClassDetector {
config: DetectorConfig,
thresholds: GodClassThresholds,
excluded_patterns: Vec<Regex>,
use_pattern_exclusions: bool,
use_graph_context: bool,
}
impl GodClassDetector {
pub fn new() -> Self {
Self::with_thresholds(GodClassThresholds::default())
}
pub fn with_thresholds(thresholds: GodClassThresholds) -> Self {
let excluded_patterns = EXCLUDED_PATTERNS
.iter()
.filter_map(|p| Regex::new(p).ok())
.collect();
Self {
config: DetectorConfig::new(),
thresholds,
excluded_patterns,
use_pattern_exclusions: true,
use_graph_context: true, }
}
pub fn with_config(config: DetectorConfig) -> Self {
use crate::calibrate::MetricKind;
let thresholds = GodClassThresholds {
max_methods: config
.get_option("max_methods")
.or_else(|| config.get_option("method_count"))
.unwrap_or_else(|| config.adaptive.warn_usize(MetricKind::ClassMethodCount, 20)),
critical_methods: config.get_option_or("critical_methods", 30),
max_lines: config
.get_option("max_lines")
.or_else(|| config.get_option("loc"))
.unwrap_or_else(|| config.adaptive.warn_usize(MetricKind::FileLength, 500)),
critical_lines: config.get_option_or(
"critical_lines",
config.adaptive.high_usize(MetricKind::FileLength, 1000),
),
max_complexity: config.get_option_or(
"max_complexity",
config.adaptive.warn_usize(MetricKind::Complexity, 100),
),
critical_complexity: config.get_option_or(
"critical_complexity",
config.adaptive.high_usize(MetricKind::Complexity, 200),
),
};
let use_pattern_exclusions = config.get_option_or("use_pattern_exclusions", true);
let use_graph_context = config.get_option_or("use_graph_context", true);
let excluded_patterns = EXCLUDED_PATTERNS
.iter()
.filter_map(|p| Regex::new(p).ok())
.collect();
Self {
config,
thresholds,
excluded_patterns,
use_pattern_exclusions,
use_graph_context,
}
}
fn is_excluded_pattern(&self, class_name: &str) -> bool {
if !self.use_pattern_exclusions {
return false;
}
self.excluded_patterns
.iter()
.any(|p| p.is_match(class_name))
}
fn build_class_contexts_fallback(
&self,
graph: &dyn crate::graph::GraphQuery,
) -> Option<ClassContextMap> {
let builder = ClassContextBuilder::new(graph);
let contexts = builder.build();
debug!("ClassContext built {} entries (fallback)", contexts.len());
if contexts.is_empty() {
debug!("ClassContext empty, falling back to pattern matching");
None
} else {
for (qn, ctx) in &contexts {
if ctx.role == ClassRole::FrameworkCore {
debug!("Framework class detected: {} ({:?})", qn, ctx.role_reason);
}
}
Some(contexts)
}
}
fn is_god_class(
&self,
method_count: usize,
complexity: usize,
loc: usize,
max_methods: usize,
critical_methods: usize,
max_lines: usize,
critical_lines: usize,
) -> Option<String> {
let mut reasons = Vec::new();
if method_count >= critical_methods {
reasons.push(format!("very high method count ({})", method_count));
} else if method_count >= max_methods {
reasons.push(format!("high method count ({})", method_count));
}
if complexity >= self.thresholds.critical_complexity {
reasons.push(format!("very high complexity ({})", complexity));
} else if complexity >= self.thresholds.max_complexity {
reasons.push(format!("high complexity ({})", complexity));
}
if loc >= critical_lines {
reasons.push(format!("very large class ({} LOC)", loc));
} else if loc >= max_lines {
reasons.push(format!("large class ({} LOC)", loc));
}
let critical_count = [
method_count >= critical_methods,
complexity >= self.thresholds.critical_complexity,
loc >= critical_lines,
]
.iter()
.filter(|&&x| x)
.count();
if critical_count >= 1 || reasons.len() >= 2 {
Some(reasons.join(", "))
} else {
None
}
}
fn calculate_severity(
&self,
method_count: usize,
complexity: usize,
loc: usize,
severity_multiplier: f64,
) -> Severity {
let critical_count = [
method_count >= self.thresholds.critical_methods,
complexity >= self.thresholds.critical_complexity,
loc >= self.thresholds.critical_lines,
]
.iter()
.filter(|&&x| x)
.count();
let high_count = [
method_count >= self.thresholds.max_methods,
complexity >= self.thresholds.max_complexity,
loc >= self.thresholds.max_lines,
]
.iter()
.filter(|&&x| x)
.count();
let base_severity = match (critical_count, high_count) {
(n, _) if n >= 2 => Severity::Critical,
(1, _) => Severity::High,
(0, n) if n >= 2 => Severity::High,
(0, 1) => Severity::Medium,
_ => Severity::Low,
};
if severity_multiplier <= 0.0 {
return Severity::Low; }
if severity_multiplier <= 0.3 {
return Severity::Low;
}
if severity_multiplier <= 0.5 {
return match base_severity {
Severity::Critical => Severity::Medium,
Severity::High => Severity::Low,
_ => Severity::Low,
};
}
if severity_multiplier <= 0.7 {
return match base_severity {
Severity::Critical => Severity::High,
Severity::High => Severity::Medium,
_ => base_severity,
};
}
base_severity
}
fn suggest_refactoring(
&self,
name: &str,
method_count: usize,
complexity: usize,
loc: usize,
role_note: Option<&str>,
) -> String {
let mut suggestions = vec![format!(
"Refactor '{}' to reduce its responsibilities:\n",
name
)];
if let Some(note) = role_note {
suggestions.push(format!("**Note:** {}\n\n", note));
}
if method_count >= self.thresholds.max_methods {
suggestions.push(
"1. **Extract related methods into separate classes**\n\
- Look for method groups that work with the same data\n\
- Create focused classes with single responsibilities\n"
.to_string(),
);
}
if complexity >= self.thresholds.max_complexity {
suggestions.push(
"2. **Simplify complex methods**\n\
- Break down complex methods into smaller functions\n\
- Consider using the Strategy or Command pattern\n"
.to_string(),
);
}
if loc >= self.thresholds.max_lines {
suggestions.push(format!(
"3. **Break down the large class ({} LOC)**\n\
- Split into smaller, focused classes\n\
- Consider using composition over inheritance\n\
- Extract data classes for complex state\n",
loc
));
}
suggestions.push(
"\n**Apply SOLID principles:**\n\
- Single Responsibility: Each class should have one reason to change\n\
- Open/Closed: Extend behavior without modifying existing code\n\
- Interface Segregation: Create specific interfaces\n\
- Dependency Inversion: Depend on abstractions"
.to_string(),
);
suggestions.join("")
}
fn estimate_effort(&self, method_count: usize, complexity: usize, loc: usize) -> String {
if method_count >= self.thresholds.critical_methods
|| complexity >= self.thresholds.critical_complexity
|| loc >= self.thresholds.critical_lines
{
"Large (1-2 weeks)".to_string()
} else if method_count >= self.thresholds.max_methods
|| complexity >= self.thresholds.max_complexity
|| loc >= self.thresholds.max_lines
{
"Medium (3-5 days)".to_string()
} else {
"Small (1-2 days)".to_string()
}
}
fn create_finding(
&self,
name: String,
file_path: String,
method_count: usize,
complexity: usize,
loc: usize,
range: Option<LineRange>,
reason: &str,
role_info: Option<(&ClassRole, &str)>,
) -> Finding {
let severity_multiplier = role_info
.map(|(role, _)| role.severity_multiplier())
.unwrap_or(1.0);
let severity = self.calculate_severity(method_count, complexity, loc, severity_multiplier);
let role_note = role_info.map(|(role, reason)| {
format!(
"This class was identified as {:?} ({}). Thresholds adjusted accordingly.",
role, reason
)
});
let description = if let Some((role, role_reason)) = role_info {
format!(
"Class '{}' shows signs of being a god class: {}.\n\n\
**Role Analysis:** {:?} — {}\n\n\
**Metrics:**\n\
- Methods: {}\n\
- Total complexity: {}\n\
- Lines of code: {}",
name, reason, role, role_reason, method_count, complexity, loc
)
} else {
format!(
"Class '{}' shows signs of being a god class: {}.\n\n\
**Metrics:**\n\
- Methods: {}\n\
- Total complexity: {}\n\
- Lines of code: {}",
name, reason, method_count, complexity, loc
)
};
let explanation = self.config.adaptive.explain(
crate::calibrate::MetricKind::ClassMethodCount,
method_count as f64,
20.0, );
let threshold_metadata = explanation.to_metadata().into_iter().collect();
let description = format!("{}\n\n📊 {}", description, explanation.to_note());
Finding {
id: String::new(),
detector: "GodClassDetector".to_string(),
severity,
title: format!("God class detected: {}", name),
description,
affected_files: vec![PathBuf::from(&file_path)],
line_start: range.map(|r| r.start),
line_end: range.map(|r| r.end),
suggested_fix: Some(self.suggest_refactoring(
&name,
method_count,
complexity,
loc,
role_note.as_deref(),
)),
estimated_effort: Some(self.estimate_effort(method_count, complexity, loc)),
category: Some("complexity".to_string()),
cwe_id: None,
why_it_matters: Some(
"God classes violate the Single Responsibility Principle. They are difficult \
to understand, test, and maintain. Changes to one part may unexpectedly \
affect other parts, leading to bugs and technical debt."
.to_string(),
),
threshold_metadata,
..Default::default()
}
}
}
impl Default for GodClassDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for GodClassDetector {
fn name(&self) -> &'static str {
"GodClassDetector"
}
fn description(&self) -> &'static str {
"Detects classes with too many methods or lines of code"
}
fn category(&self) -> &'static str {
"complexity"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn detect(
&self,
ctx: &crate::detectors::analysis_context::AnalysisContext,
) -> Result<Vec<Finding>> {
let graph = ctx.graph;
let i = graph.interner();
let mut findings = Vec::new();
let fallback_contexts: Option<ClassContextMap>;
let class_contexts: Option<&ClassContextMap> = if self.use_graph_context {
if let Some(ref prebuilt) = ctx.detector_ctx.class_contexts {
debug!("ClassContext using pre-built {} entries", prebuilt.len());
if prebuilt.is_empty() {
None
} else {
Some(prebuilt.as_ref())
}
} else {
fallback_contexts = self.build_class_contexts_fallback(graph);
fallback_contexts.as_ref()
}
} else {
None
};
for class in graph.get_classes_shared().iter() {
if class.qn(i).contains("::interface::") || class.qn(i).contains("::type::") {
continue;
}
let class_name = class.node_name(i);
{
let lower_name = class_name.to_lowercase();
if lower_name.ends_with("test")
|| lower_name.ends_with("tests")
|| lower_name.ends_with("spec")
|| lower_name.ends_with("testcase")
|| lower_name.ends_with("testsuite")
{
debug!("Skipping test class by name: {}", class_name);
continue;
}
}
let method_count = class.get_i64("methodCount").unwrap_or(0) as usize;
let complexity = class.complexity_opt().unwrap_or(1) as usize;
let loc = class.loc() as usize;
let ctx = class_contexts.and_then(|c| c.get(class.qn(i)));
if let Some(ctx) = ctx {
if ctx.skip_god_class() || ctx.is_test {
debug!(
"Skipping {} ({:?}): {}",
class_name, ctx.role, ctx.role_reason
);
continue;
}
}
if ctx.is_none() && self.is_excluded_pattern(class_name) {
debug!("Skipping excluded pattern: {}", class_name);
continue;
}
if ctx.is_none() {
let lower_path = class.path(i).to_lowercase();
if lower_path.contains("/test/") || lower_path.contains("/tests/")
|| lower_path.contains("/__tests__/") || lower_path.contains("/spec/")
|| lower_path.contains("test_") || lower_path.contains("_test.")
|| lower_path.starts_with("tests/")
|| lower_path.starts_with("test/")
{
debug!("Skipping test class: {}", class_name);
continue;
}
}
let lang_multiplier: usize = {
let ext = std::path::Path::new(class.path(i))
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"java" | "cs" => 2,
_ => 1,
}
};
let (max_methods, max_lines) = ctx
.map(|c| {
c.adjusted_thresholds(self.thresholds.max_methods, self.thresholds.max_lines)
})
.unwrap_or((self.thresholds.max_methods, self.thresholds.max_lines));
let max_methods = max_methods * lang_multiplier;
let max_lines = max_lines * lang_multiplier;
let (critical_methods, critical_lines) = ctx
.map(|c| {
c.adjusted_thresholds(
self.thresholds.critical_methods,
self.thresholds.critical_lines,
)
})
.unwrap_or((
self.thresholds.critical_methods,
self.thresholds.critical_lines,
));
let critical_methods = critical_methods * lang_multiplier;
let critical_lines = critical_lines * lang_multiplier;
if let Some(reason) = self.is_god_class(
method_count,
complexity,
loc,
max_methods,
critical_methods,
max_lines,
critical_lines,
) {
let role_info = ctx.map(|c| (&c.role, c.role_reason.as_str()));
findings.push(self.create_finding(
class.node_name(i).to_string(),
class.path(i).to_string(),
method_count,
complexity,
loc,
Some(LineRange::new(class.line_start, class.line_end)),
&reason,
role_info,
));
}
}
info!("GodClassDetector: found {} issues", findings.len());
Ok(findings)
}
}
impl crate::detectors::RegisteredDetector for GodClassDetector {
fn create(init: &crate::detectors::DetectorInit) -> std::sync::Arc<dyn Detector> {
std::sync::Arc::new(Self::with_config(init.config_for("GodClassDetector")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::builder::GraphBuilder;
use crate::graph::CodeNode;
fn create_test_class(name: &str, methods: usize, loc: u32, complexity: i64) -> CodeNode {
CodeNode::class(name, "test.py")
.with_qualified_name(&format!("test::{}", name))
.with_lines(1, loc)
.with_property("methodCount", methods as i64)
.with_property("complexity", complexity)
}
#[test]
fn test_skip_framework_class() {
let mut store = GraphBuilder::new();
store.add_node(create_test_class("Flask", 50, 2000, 150));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Framework core class Flask should not be flagged"
);
}
#[test]
fn test_skip_application_pattern() {
let mut store = GraphBuilder::new();
store.add_node(create_test_class("MyApplication", 40, 1500, 120));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Class matching Application pattern should not be flagged"
);
}
#[test]
fn test_flag_actual_god_class() {
let mut store = GraphBuilder::new();
store.add_node(create_test_class("OrderProcessor", 35, 1200, 180));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert_eq!(findings.len(), 1, "Actual god class should be flagged");
assert!(findings[0].title.contains("OrderProcessor"));
}
#[test]
fn test_thresholds() {
let detector = GodClassDetector::new();
assert!(detector
.is_god_class(19, 99, 499, 20, 30, 500, 1000)
.is_none());
assert!(detector
.is_god_class(20, 50, 400, 20, 30, 500, 1000)
.is_none());
assert!(detector
.is_god_class(25, 120, 400, 20, 30, 500, 1000)
.is_some());
assert!(detector
.is_god_class(30, 50, 400, 20, 30, 500, 1000)
.is_some());
assert!(detector
.is_god_class(25, 120, 700, 20, 30, 500, 1000)
.is_some());
}
#[test]
fn test_excluded_patterns() {
let detector = GodClassDetector::new();
assert!(detector.is_excluded_pattern("DatabaseClient"));
assert!(detector.is_excluded_pattern("UserManager"));
assert!(detector.is_excluded_pattern("EventFacade"));
assert!(!detector.is_excluded_pattern("OrderProcessor"));
}
fn create_java_class(name: &str, methods: usize, loc: u32, complexity: i64) -> CodeNode {
CodeNode::class(name, "src/main/java/com/example/Service.java")
.with_qualified_name(&format!("com.example::{}", name))
.with_lines(1, loc)
.with_property("methodCount", methods as i64)
.with_property("complexity", complexity)
}
#[test]
fn test_java_class_below_2x_threshold_not_flagged() {
let mut store = GraphBuilder::new();
store.add_node(create_java_class("UserService", 35, 800, 80));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Java class with 35 methods should not be flagged (threshold is 2x = 40), got: {:?}",
findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
}
#[test]
fn test_java_class_above_2x_threshold_flagged() {
let mut store = GraphBuilder::new();
store.add_node(create_java_class("MegaService", 45, 1200, 220));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert_eq!(
findings.len(),
1,
"Java class with 45 methods and 1200 LOC should be flagged (above 2x threshold)"
);
assert!(findings[0].title.contains("MegaService"));
}
#[test]
fn test_skips_test_class_by_name() {
let mut store = GraphBuilder::new();
store.add_node(create_test_class("MapInterfaceTest", 60, 2000, 200));
store.add_node(create_test_class("CollectionTests", 50, 1500, 180));
store.add_node(create_test_class("BehaviorSpec", 40, 1000, 150));
let detector = GodClassDetector::new();
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Test classes (names ending in Test/Tests/Spec) should be skipped, got: {:?}",
findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
}
}