portail 2.1.0

Unified proxy/gateway: AI Gateway + MCP Gateway + CDN cache
Documentation
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() {
            // Find complexity annotations
            for cap in complexity_re.captures_iter(line) {
                annotations.push(ComplexityAnnotation {
                    file: path_str.clone(),
                    line: line_num + 1,
                    complexity: cap[0].to_string(),
                });
            }

            // Find function definitions
            for cap in function_re.captures_iter(line) {
                functions.push(FunctionInfo {
                    file: path_str.clone(),
                    line: line_num + 1,
                    name: cap[1].to_string(),
                });
            }
        }
    }

    // Calculate distribution
    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")
    ));

    // Summary
    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
    ));

    // Distribution
    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');

    // Annotations
    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');

    // High complexity functions
    let high_complexity: Vec<_> = report
        .annotations
        .iter()
        .filter(|a| {
            a.complexity.contains("")
                || a.complexity.contains("")
                || 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"));
    }
}