use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::debug;
use super::Tool;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ComplexityLevel {
Low,
Medium,
High,
Critical,
}
impl ComplexityLevel {
pub fn from_score(score: u32) -> Self {
match score {
1..=10 => Self::Low,
11..=20 => Self::Medium,
21..=50 => Self::High,
_ => Self::Critical,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
pub fn suggestion(&self) -> &'static str {
match self {
Self::Low => "Complexity is good. No action needed.",
Self::Medium => "Consider breaking into smaller functions if readability suffers.",
Self::High => "Should be refactored. Extract helper functions to reduce complexity.",
Self::Critical => "Critical complexity! Immediate refactoring required. Split into multiple functions.",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionMetrics {
pub name: String,
pub line_number: usize,
pub complexity: u32,
pub level: String,
pub suggestion: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeMetricsReport {
pub file: String,
pub functions: Vec<FunctionMetrics>,
pub summary: MetricsSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSummary {
pub total_functions: usize,
pub avg_complexity: f64,
pub max_complexity: u32,
pub level_counts: std::collections::HashMap<String, usize>,
}
pub struct CodeMetricsTool;
impl CodeMetricsTool {
pub fn new() -> Self {
Self
}
fn calculate_complexity(function_body: &str) -> u32 {
let mut complexity: u32 = 1;
let body_lower = function_body.to_lowercase();
complexity += body_lower.matches("if ").count() as u32;
complexity += body_lower.matches("else").count() as u32;
complexity += body_lower.matches("match ").count() as u32;
complexity += body_lower.matches("loop").count() as u32;
complexity += body_lower.matches("for ").count() as u32;
complexity += body_lower.matches("while ").count() as u32;
complexity += body_lower.matches(" && ").count() as u32;
complexity += body_lower.matches(" || ").count() as u32;
complexity += body_lower.matches("?").count() as u32;
complexity += body_lower.matches("return ").count() as u32;
complexity
}
fn analyze_function(name: &str, body: &str, line_number: usize) -> FunctionMetrics {
let complexity = Self::calculate_complexity(body);
let level = ComplexityLevel::from_score(complexity);
FunctionMetrics {
name: name.to_string(),
line_number,
complexity,
level: level.as_str().to_string(),
suggestion: level.suggestion().to_string(),
}
}
}
impl Default for CodeMetricsTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for CodeMetricsTool {
fn name(&self) -> &str {
"code_metrics"
}
fn description(&self) -> &str {
"Analyze a Rust source file and report cyclomatic complexity for each function. \
Returns a JSON report with complexity scores, classifications, and refactoring \
suggestions. Use this to identify complex functions that may need simplification."
}
fn schema(&self) -> Value {
serde_json::json!({
"type": "object",
"required": ["file_path"],
"properties": {
"file_path": {
"type": "string",
"description": "Path to the Rust source file to analyze"
}
}
})
}
async fn execute(&self, args: Value) -> Result<Value> {
let file_path = args
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required 'file_path' argument"))?;
debug!("Analyzing code metrics for: {}", file_path);
let content = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {}", file_path))?;
let parsed = crate::tools::introspect::parser::parse_rust(&content);
let mut functions: Vec<FunctionMetrics> = Vec::new();
for symbol in &parsed.symbols {
if let crate::tools::introspect::parser::SymbolKind::Function = symbol.kind {
let body_start = symbol.line_start;
let lines: Vec<&str> = content.lines().collect();
let body_end = (body_start + 30).min(lines.len());
let body = if body_start < lines.len() {
lines[body_start.saturating_sub(1)..body_end].join("\n")
} else {
symbol.signature.clone()
};
let metrics = Self::analyze_function(&symbol.name, &body, symbol.line_start);
functions.push(metrics);
}
}
let total_functions = functions.len();
let total_complexity: u32 = functions.iter().map(|f| f.complexity).sum();
let avg_complexity = if total_functions > 0 {
total_complexity as f64 / total_functions as f64
} else {
0.0
};
let max_complexity = functions.iter().map(|f| f.complexity).max().unwrap_or(0);
let mut level_counts = std::collections::HashMap::new();
for func in &functions {
*level_counts.entry(func.level.clone()).or_insert(0) += 1;
}
let report = CodeMetricsReport {
file: file_path.to_string(),
functions,
summary: MetricsSummary {
total_functions,
avg_complexity,
max_complexity,
level_counts,
},
};
Ok(serde_json::to_value(report)?)
}
}
#[cfg(test)]
#[path = "../../tests/unit/tools/code_metrics/code_metrics_test.rs"]
mod tests;