use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphClient;
use crate::models::{Finding, Severity};
use anyhow::Result;
use std::path::PathBuf;
use tracing::{debug, info};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct MiddleManThresholds {
pub min_delegation_methods: usize,
pub delegation_threshold: f64,
pub max_complexity: usize,
}
impl Default for MiddleManThresholds {
fn default() -> Self {
Self {
min_delegation_methods: 3,
delegation_threshold: 0.7,
max_complexity: 2,
}
}
}
pub struct MiddleManDetector {
config: DetectorConfig,
thresholds: MiddleManThresholds,
}
impl MiddleManDetector {
pub fn new() -> Self {
Self::with_thresholds(MiddleManThresholds::default())
}
pub fn with_thresholds(thresholds: MiddleManThresholds) -> Self {
Self {
config: DetectorConfig::new(),
thresholds,
}
}
pub fn with_config(config: DetectorConfig) -> Self {
let thresholds = MiddleManThresholds {
min_delegation_methods: config.get_option_or("min_delegation_methods", 3),
delegation_threshold: config.get_option_or("delegation_threshold", 0.7),
max_complexity: config.get_option_or("max_complexity", 2),
};
Self { config, thresholds }
}
fn calculate_severity(&self, delegation_pct: f64) -> Severity {
if delegation_pct >= 90.0 {
Severity::High
} else if delegation_pct >= 70.0 {
Severity::Medium
} else {
Severity::Low
}
}
fn estimate_effort(&self, severity: Severity) -> String {
match severity {
Severity::Critical | Severity::High => "Medium (1-2 hours)".to_string(),
Severity::Medium => "Small (30-60 minutes)".to_string(),
Severity::Low | Severity::Info => "Small (15-30 minutes)".to_string(),
}
}
fn create_finding(
&self,
_class_name: String,
class_simple: String,
_target_class: String,
target_name: String,
file_path: String,
line_start: Option<u32>,
line_end: Option<u32>,
delegation_count: usize,
total_methods: usize,
) -> Finding {
let delegation_pct = (delegation_count as f64 / total_methods as f64) * 100.0;
let severity = self.calculate_severity(delegation_pct);
let suggestion = if delegation_pct >= 90.0 {
format!(
"Class '{}' delegates {:.0}% of methods ({}/{}) to '{}'. Consider:\n\
1. Remove the middle man and use '{}' directly\n\
2. If this is a facade, add value by combining operations\n\
3. Document the architectural reason if delegation is intentional",
class_simple,
delegation_pct,
delegation_count,
total_methods,
target_name,
target_name
)
} else {
format!(
"Class '{}' delegates {:.0}% of methods to '{}'. \
Consider whether this indirection adds value.",
class_simple, delegation_pct, target_name
)
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "MiddleManDetector".to_string(),
severity,
title: format!("Middle Man: {}", class_simple),
description: format!(
"Class '{}' acts as a middle man, delegating {} out of {} methods \
({:.0}%) to '{}' without adding significant value.\n\n\
This pattern adds unnecessary indirection and increases maintenance burden. \
Simple delegation methods with low complexity suggest the class may not be needed.",
class_simple, delegation_count, total_methods, delegation_pct, target_name
),
affected_files: vec![PathBuf::from(&file_path)],
line_start,
line_end,
suggested_fix: Some(suggestion),
estimated_effort: Some(self.estimate_effort(severity)),
category: Some("design".to_string()),
cwe_id: None,
why_it_matters: Some(
"Middle man classes add unnecessary indirection without providing value. \
They make the codebase harder to navigate, increase call stack depth, \
and add maintenance overhead. If a class only forwards calls to another, \
consider removing it or giving it real responsibilities."
.to_string(),
),
}
}
}
impl Default for MiddleManDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for MiddleManDetector {
fn name(&self) -> &'static str {
"MiddleManDetector"
}
fn description(&self) -> &'static str {
"Detects classes that mostly delegate to other classes"
}
fn category(&self) -> &'static str {
"design"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn detect(&self, graph: &GraphClient) -> Result<Vec<Finding>> {
debug!("Starting middle man detection");
let query = r#"
// First count total methods per class
MATCH (c:Class)-[:CONTAINS]->(all_m:Function)
WHERE all_m.is_method = true
WITH c, count(all_m) as total_methods
WHERE total_methods > 0
// Find delegation patterns
MATCH (c)-[:CONTAINS]->(m:Function)
WHERE m.is_method = true
AND (m.complexity IS NULL OR m.complexity <= $max_complexity)
MATCH (m)-[:CALLS]->(delegated:Function)
MATCH (delegated)<-[:CONTAINS]-(target:Class)
WHERE c <> target
WITH c, target, total_methods,
count(DISTINCT m) as delegation_count
// Filter based on thresholds
WHERE delegation_count >= $min_delegation_methods
AND cast(delegation_count, "DOUBLE") / total_methods >= $delegation_threshold
RETURN c.qualifiedName as middle_man,
c.name as class_name,
c.filePath as file_path,
c.lineStart as line_start,
c.lineEnd as line_end,
target.qualifiedName as delegates_to,
target.name as target_name,
delegation_count,
total_methods,
cast(delegation_count * 100, "DOUBLE") / total_methods as delegation_percentage
ORDER BY delegation_percentage DESC
LIMIT 50
"#;
let _params = serde_json::json!({
"min_delegation_methods": self.thresholds.min_delegation_methods,
"delegation_threshold": self.thresholds.delegation_threshold,
"max_complexity": self.thresholds.max_complexity,
});
let results = graph.execute(query)?;
if results.is_empty() {
debug!("No middle man classes found");
return Ok(vec![]);
}
let mut findings = Vec::new();
for row in results {
let class_name = row
.get("middle_man")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let class_simple = row
.get("class_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let target_class = row
.get("delegates_to")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let target_name = row
.get("target_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let file_path = row
.get("file_path")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let line_start = row
.get("line_start")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let line_end = row
.get("line_end")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let delegation_count = row
.get("delegation_count")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let total_methods = row
.get("total_methods")
.and_then(|v| v.as_u64())
.unwrap_or(1) as usize;
findings.push(self.create_finding(
class_name,
class_simple,
target_class,
target_name,
file_path,
line_start,
line_end,
delegation_count,
total_methods,
));
}
findings.sort_by(|a, b| b.severity.cmp(&a.severity));
info!(
"MiddleManDetector found {} classes acting as middle men",
findings.len()
);
Ok(findings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_thresholds() {
let detector = MiddleManDetector::new();
assert_eq!(detector.thresholds.min_delegation_methods, 3);
assert!((detector.thresholds.delegation_threshold - 0.7).abs() < f64::EPSILON);
assert_eq!(detector.thresholds.max_complexity, 2);
}
#[test]
fn test_severity_calculation() {
let detector = MiddleManDetector::new();
assert_eq!(detector.calculate_severity(60.0), Severity::Low);
assert_eq!(detector.calculate_severity(70.0), Severity::Medium);
assert_eq!(detector.calculate_severity(85.0), Severity::Medium);
assert_eq!(detector.calculate_severity(90.0), Severity::High);
assert_eq!(detector.calculate_severity(100.0), Severity::High);
}
}