use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphStore;
use crate::models::{Finding, Severity};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use tracing::{debug, info};
use uuid::Uuid;
pub struct DegreeCentralityDetector {
config: DetectorConfig,
high_complexity_threshold: u32,
high_percentile: f64,
min_indegree: usize,
min_outdegree: usize,
}
impl DegreeCentralityDetector {
pub fn new() -> Self {
Self {
config: DetectorConfig::new(),
high_complexity_threshold: 15,
high_percentile: 95.0,
min_indegree: 5,
min_outdegree: 21, }
}
pub fn with_config(config: DetectorConfig) -> Self {
Self {
high_complexity_threshold: config.get_option_or("high_complexity_threshold", 15),
high_percentile: config.get_option_or("high_percentile", 95.0),
min_indegree: config.get_option_or("min_indegree", 5),
min_outdegree: config.get_option_or("min_outdegree", 21),
config,
}
}
fn create_god_class_finding(
&self,
name: &str,
qualified_name: &str,
file_path: &str,
in_degree: usize,
out_degree: usize,
complexity: u32,
loc: u32,
max_in_degree: usize,
threshold: usize,
) -> Finding {
let percentile = if max_in_degree > 0 {
(in_degree as f64 / max_in_degree as f64) * 100.0
} else {
0.0
};
let severity = if complexity >= self.high_complexity_threshold * 2 || percentile >= 99.0 {
Severity::Critical
} else if complexity >= (self.high_complexity_threshold * 3 / 2) || percentile >= 97.0 {
Severity::High
} else {
Severity::Medium
};
let description = format!(
"File `{}` is a potential **God Class**: high in-degree \
({} dependents) combined with high complexity ({}).\n\n\
**What this means:**\n\
- Many functions depend on this code ({} callers)\n\
- The code itself is complex (complexity: {})\n\
- Changes are high-risk with wide blast radius\n\
- This is a maintainability bottleneck\n\n\
**Metrics:**\n\
- In-degree: {} (threshold: {})\n\
- Complexity: {}\n\
- Lines of code: {}\n\
- Out-degree: {}",
name,
in_degree,
complexity,
in_degree,
complexity,
in_degree,
threshold,
complexity,
loc,
out_degree
);
let suggested_fix = "\
**For God Classes:**\n\n\
1. **Extract interfaces**: Define contracts to reduce coupling\n\n\
2. **Split responsibilities**: Break into focused modules using SRP\n\n\
3. **Use dependency injection**: Reduce direct imports\n\n\
4. **Add abstraction layers**: Shield dependents from changes\n\n\
5. **Prioritize test coverage**: High-risk code needs safety net"
.to_string();
let estimated_effort = match severity {
Severity::Critical => "Large (1-2 days)",
Severity::High => "Large (4-8 hours)",
_ => "Medium (2-4 hours)",
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "DegreeCentralityDetector".to_string(),
severity,
title: format!("God Class: {}", name),
description,
affected_files: vec![file_path.into()],
line_start: None,
line_end: None,
suggested_fix: Some(suggested_fix),
estimated_effort: Some(estimated_effort.to_string()),
category: Some("architecture".to_string()),
cwe_id: None,
why_it_matters: Some(
"God Classes violate the Single Responsibility Principle. \
They accumulate too many responsibilities, making them hard to \
understand, test, and maintain."
.to_string(),
),
..Default::default()
}
}
fn create_feature_envy_finding(
&self,
name: &str,
qualified_name: &str,
file_path: &str,
in_degree: usize,
out_degree: usize,
complexity: u32,
loc: u32,
max_out_degree: usize,
threshold: usize,
) -> Finding {
let percentile = if max_out_degree > 0 {
(out_degree as f64 / max_out_degree as f64) * 100.0
} else {
0.0
};
let severity = if percentile >= 99.0 {
Severity::High
} else if percentile >= 97.0 {
Severity::Medium
} else {
Severity::Low
};
let description = format!(
"Function `{}` shows **Feature Envy**: calls {} other functions, \
suggesting it reaches into too many modules.\n\n\
**What this means:**\n\
- This function depends on {} other functions\n\
- May be handling responsibilities that belong elsewhere\n\
- Tight coupling makes changes cascade\n\
- Could be a 'God Module' orchestrating everything\n\n\
**Metrics:**\n\
- Out-degree: {} (threshold: {})\n\
- In-degree: {}\n\
- Complexity: {}\n\
- Lines of code: {}",
name, out_degree, out_degree, out_degree, threshold, in_degree, complexity, loc
);
let suggested_fix = "\
**For Feature Envy:**\n\n\
1. **Move logic to data**: Put behavior where data lives\n\n\
2. **Extract classes**: Group related functionality\n\n\
3. **Use delegation**: Have other modules handle their own logic\n\n\
4. **Review module boundaries**: This may be misplaced code\n\n\
5. **Apply facade pattern**: If orchestration is needed, make it explicit"
.to_string();
let estimated_effort = match severity {
Severity::High => "Medium (2-4 hours)",
Severity::Medium => "Medium (1-2 hours)",
_ => "Small (30-60 minutes)",
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "DegreeCentralityDetector".to_string(),
severity,
title: format!("Feature Envy: {}", name),
description,
affected_files: vec![file_path.into()],
line_start: None,
line_end: None,
suggested_fix: Some(suggested_fix),
estimated_effort: Some(estimated_effort.to_string()),
category: Some("coupling".to_string()),
cwe_id: None,
why_it_matters: Some(
"Feature Envy occurs when a function uses features of other classes \
more than its own. This creates tight coupling and makes the code \
harder to maintain and test."
.to_string(),
),
..Default::default()
}
}
fn create_coupling_hotspot_finding(
&self,
name: &str,
qualified_name: &str,
file_path: &str,
in_degree: usize,
out_degree: usize,
complexity: u32,
loc: u32,
) -> Finding {
let total_coupling = in_degree + out_degree;
let severity = if complexity >= self.high_complexity_threshold {
Severity::Critical
} else {
Severity::High
};
let description = format!(
"Function `{}` is a **Coupling Hotspot**: high in-degree ({}) \
AND high out-degree ({}).\n\n\
**What this means:**\n\
- Both heavily depended ON ({} callers)\n\
- AND heavily dependent ON others ({} callees)\n\
- Total coupling: {} connections\n\
- Changes here cascade in both directions\n\
- This is a critical architectural risk\n\n\
**Metrics:**\n\
- In-degree: {}\n\
- Out-degree: {}\n\
- Total coupling: {}\n\
- Complexity: {}\n\
- Lines of code: {}",
name,
in_degree,
out_degree,
in_degree,
out_degree,
total_coupling,
in_degree,
out_degree,
total_coupling,
complexity,
loc
);
let suggested_fix = "\
**For Coupling Hotspots (Critical):**\n\n\
1. **Architectural review**: This function is a design bottleneck\n\n\
2. **Split by responsibility**: Extract into focused modules\n\n\
3. **Introduce layers**: Create abstraction boundaries\n\n\
4. **Apply SOLID principles**:\n\
- Single Responsibility (split concerns)\n\
- Interface Segregation (smaller interfaces)\n\
- Dependency Inversion (depend on abstractions)\n\n\
5. **Consider strangler pattern**: Gradually replace with better design"
.to_string();
let estimated_effort = if severity == Severity::Critical {
"Large (1-2 days)"
} else {
"Large (4-8 hours)"
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "DegreeCentralityDetector".to_string(),
severity,
title: format!("Coupling Hotspot: {}", name),
description,
affected_files: vec![file_path.into()],
line_start: None,
line_end: None,
suggested_fix: Some(suggested_fix),
estimated_effort: Some(estimated_effort.to_string()),
category: Some("architecture".to_string()),
cwe_id: None,
why_it_matters: Some(
"Coupling hotspots are the most problematic code - they both depend on \
many other parts AND are depended on by many parts. Any change here \
cascades in all directions."
.to_string(),
),
..Default::default()
}
}
}
impl Default for DegreeCentralityDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for DegreeCentralityDetector {
fn name(&self) -> &'static str {
"DegreeCentralityDetector"
}
fn description(&self) -> &'static str {
"Detects coupling issues using degree centrality (God Classes, Feature Envy, Coupling Hotspots)"
}
fn category(&self) -> &'static str {
"coupling"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
} fn detect(&self, graph: &GraphStore) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
const SKIP_NAMES: &[&str] = &[
"new", "default", "from", "into", "create", "build", "make", "with",
"clone", "drop", "fmt", "eq", "hash", "cmp", "partial_cmp",
"get", "set", "instance", "global", "shared", "current",
"run", "main", "init", "setup", "start", "execute", "dispatch", "handle",
"read", "write", "parse", "format", "render", "display", "detect", "analyze",
"iter", "next", "map", "filter", "fold",
];
const SKIP_PATHS: &[&str] = &[
"/mod.rs", "/lib.rs", "/main.rs", "/cli/", "/handlers/",
];
for func in graph.get_functions() {
let name_lower = func.name.to_lowercase();
if SKIP_NAMES.iter().any(|&skip| name_lower == skip || name_lower.starts_with(&format!("{}_", skip))) {
continue;
}
if SKIP_PATHS.iter().any(|&pat| func.file_path.contains(pat)) {
continue;
}
let fan_in = graph.call_fan_in(&func.qualified_name);
let fan_out = graph.call_fan_out(&func.qualified_name);
let total_degree = fan_in + fan_out;
if fan_in > 20 && fan_out < 5 {
continue; }
if fan_out > 20 && fan_in < 5 {
continue; }
if total_degree >= 30 && fan_in >= 8 && fan_out >= 8 {
let severity = if total_degree >= 60 && fan_in >= 15 && fan_out >= 15 {
Severity::High
} else if total_degree >= 40 {
Severity::Medium
} else {
Severity::Low
};
findings.push(Finding {
id: Uuid::new_v4().to_string(),
detector: "DegreeCentralityDetector".to_string(),
severity,
title: format!("High Coupling: {}", func.name),
description: format!(
"Function '{}' has {} connections ({} callers, {} callees). High coupling increases change risk.",
func.name, total_degree, fan_in, fan_out
),
affected_files: vec![func.file_path.clone().into()],
line_start: Some(func.line_start),
line_end: Some(func.line_end),
suggested_fix: Some("Consider breaking into smaller functions or using dependency injection".to_string()),
estimated_effort: Some("Medium (2-4 hours)".to_string()),
category: Some("coupling".to_string()),
cwe_id: None,
why_it_matters: Some("Highly coupled code is harder to change and test".to_string()),
..Default::default()
});
}
}
Ok(findings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_detector() {
let detector = DegreeCentralityDetector::new();
assert_eq!(detector.high_complexity_threshold, 15);
assert_eq!(detector.min_indegree, 5);
}
#[test]
fn test_with_config() {
let config = DetectorConfig::new()
.with_option("high_complexity_threshold", serde_json::json!(25))
.with_option("min_indegree", serde_json::json!(10));
let detector = DegreeCentralityDetector::with_config(config);
assert_eq!(detector.high_complexity_threshold, 25);
assert_eq!(detector.min_indegree, 10);
}
}