use anyhow::Result;
use regex::Regex;
use serde::Serialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize)]
pub struct ComplexityReport {
pub files_scanned: usize,
pub total_functions: usize,
pub total_annotations: usize,
pub annotations: Vec<ComplexityAnnotation>,
pub functions: Vec<FunctionInfo>,
pub distribution: Vec<(String, usize)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ComplexityAnnotation {
pub file: String,
pub line: usize,
pub complexity: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct FunctionInfo {
pub file: String,
pub line: usize,
pub name: String,
}
pub fn analyze_directory(dir: &Path) -> Result<ComplexityReport> {
let complexity_re = Regex::new(r"O\([^)]+\)")?;
let function_re = Regex::new(r"fn\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
let mut annotations = Vec::new();
let mut functions = Vec::new();
let mut files_scanned = 0;
for entry in walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
{
let path = entry.path();
let content = fs::read_to_string(path)?;
files_scanned += 1;
let path_str = path.display().to_string();
for (line_num, line) in content.lines().enumerate() {
for cap in complexity_re.captures_iter(line) {
annotations.push(ComplexityAnnotation {
file: path_str.clone(),
line: line_num + 1,
complexity: cap[0].to_string(),
});
}
for cap in function_re.captures_iter(line) {
functions.push(FunctionInfo {
file: path_str.clone(),
line: line_num + 1,
name: cap[1].to_string(),
});
}
}
}
let mut distribution = std::collections::HashMap::new();
for ann in &annotations {
*distribution.entry(ann.complexity.clone()).or_insert(0) += 1;
}
let mut distribution: Vec<_> = distribution.into_iter().collect();
distribution.sort_by_key(|b| std::cmp::Reverse(b.1));
Ok(ComplexityReport {
files_scanned,
total_functions: functions.len(),
total_annotations: annotations.len(),
annotations,
functions,
distribution,
})
}
pub fn generate_report(report: &ComplexityReport) -> String {
let mut output = String::new();
output.push_str("# Time Complexity Analysis\n\n");
output.push_str(&format!(
"Generated: {}\n\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
output.push_str("## Summary\n\n");
output.push_str(&format!("- **Files scanned:** {}\n", report.files_scanned));
output.push_str(&format!(
"- **Total functions:** {}\n",
report.total_functions
));
output.push_str(&format!(
"- **Complexity annotations:** {}\n\n",
report.total_annotations
));
output.push_str("## Complexity Distribution\n\n");
output.push_str("| Complexity | Count |\n");
output.push_str("|------------|-------|\n");
for (complexity, count) in &report.distribution {
output.push_str(&format!("| {} | {} |\n", complexity, count));
}
output.push('\n');
output.push_str("## Complexity Annotations\n\n");
output.push_str("| File | Line | Complexity |\n");
output.push_str("|------|------|------------|\n");
for ann in &report.annotations {
output.push_str(&format!(
"| {} | {} | {} |\n",
ann.file, ann.line, ann.complexity
));
}
output.push('\n');
let high_complexity: Vec<_> = report
.annotations
.iter()
.filter(|a| {
a.complexity.contains("n²")
|| a.complexity.contains("n³")
|| a.complexity.contains("2^n")
|| a.complexity.contains("n!")
})
.collect();
if !high_complexity.is_empty() {
output.push_str("## High Complexity Functions\n\n");
for ann in high_complexity {
output.push_str(&format!(
"- `{}:{}`: {}\n",
ann.file, ann.line, ann.complexity
));
}
output.push('\n');
}
output.push_str("---\n*Report generated by portail complexity analysis*\n");
output
}
pub fn analyze_and_report(dir: &Path, output_path: Option<&Path>) -> Result<String> {
let report = analyze_directory(dir)?;
let report_text = generate_report(&report);
if let Some(path) = output_path {
fs::write(path, &report_text)?;
}
Ok(report_text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complexity_regex() {
let re = Regex::new(r"O\([^)]+\)").unwrap();
assert!(re.is_match("O(n)"));
assert!(re.is_match("O(n²)"));
assert!(re.is_match("O(1)"));
assert!(!re.is_match("no complexity"));
}
#[test]
fn test_function_regex() {
let re = Regex::new(r"fn\s+([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
assert!(re.is_match("fn my_function()"));
assert!(re.is_match("pub fn test()"));
assert!(!re.is_match("not a function"));
}
}