Skip to main content

cargo_mate/tools/
code_analyzer.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::collections::HashMap;
5use std::path::Path;
6use std::fs;
7use regex::Regex;
8use walkdir::WalkDir;
9use serde::{Serialize, Deserialize};
10#[derive(Debug, Clone)]
11pub struct CodeAnalyzer;
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FunctionMetrics {
14    pub name: String,
15    pub file: String,
16    pub lines: usize,
17    pub complexity: f64,
18    pub parameters: usize,
19    pub is_public: bool,
20    pub is_async: bool,
21    pub return_type: String,
22}
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CodeAnalysis {
25    pub total_functions: usize,
26    pub public_functions: usize,
27    pub async_functions: usize,
28    pub average_complexity: f64,
29    pub total_lines: usize,
30    pub largest_function: Option<FunctionMetrics>,
31    pub functions: Vec<FunctionMetrics>,
32}
33impl CodeAnalyzer {
34    pub fn new() -> Self {
35        Self
36    }
37    fn parse_rust_functions(&self, file_path: &str) -> Result<Vec<FunctionMetrics>> {
38        let content = fs::read_to_string(file_path)
39            .map_err(|e| {
40                ToolError::InvalidArguments(
41                    format!("Failed to read {}: {}", file_path, e),
42                )
43            })?;
44        let mut functions = Vec::new();
45        let function_regex = Regex::new(
46                r"(?s)(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*([^;{]*?))?\s*\{([^}]*)\}",
47            )
48            .unwrap();
49        for captures in function_regex.captures_iter(&content) {
50            let function_name = captures[1].to_string();
51            let parameters = captures.get(2).map_or("", |m| m.as_str());
52            let return_type = captures.get(3).map_or("()", |m| m.as_str().trim());
53            let function_body = captures.get(4).map_or("", |m| m.as_str());
54            let metrics = FunctionMetrics {
55                name: function_name.clone(),
56                file: file_path.to_string(),
57                lines: function_body.lines().count(),
58                complexity: self.calculate_complexity(function_body),
59                parameters: self.count_parameters(parameters),
60                is_public: function_name.starts_with("pub")
61                    || content.contains(&format!("pub fn {}", function_name)),
62                is_async: content.contains("async fn")
63                    && content.contains(&function_name),
64                return_type: return_type.to_string(),
65            };
66            functions.push(metrics);
67        }
68        Ok(functions)
69    }
70    fn calculate_complexity(&self, code: &str) -> f64 {
71        let lines = code.lines().count() as f64;
72        let branches = code.matches("if").count() as f64
73            + code.matches("match").count() as f64;
74        let loops = code.matches("for").count() + code.matches("while").count()
75            + code.matches("loop").count();
76        lines * 0.1 + branches * 0.5 + loops as f64 * 0.8
77    }
78    fn count_parameters(&self, params: &str) -> usize {
79        if params.trim().is_empty() {
80            return 0;
81        }
82        params.split(',').filter(|p| !p.trim().is_empty()).count()
83    }
84    fn analyze_codebase(&self, path: &str) -> Result<CodeAnalysis> {
85        let mut all_functions = Vec::new();
86        if Path::new(path).is_file() {
87            if path.ends_with(".rs") {
88                all_functions.extend(self.parse_rust_functions(path)?);
89            }
90        } else {
91            for entry in WalkDir::new(path)
92                .into_iter()
93                .filter_map(|e| e.ok())
94                .filter(|e| e.path().extension().map_or(false, |ext| ext == "rs"))
95            {
96                all_functions
97                    .extend(self.parse_rust_functions(&entry.path().to_string_lossy())?);
98            }
99        }
100        let total_functions = all_functions.len();
101        let public_functions = all_functions.iter().filter(|f| f.is_public).count();
102        let async_functions = all_functions.iter().filter(|f| f.is_async).count();
103        let total_lines = all_functions.iter().map(|f| f.lines).sum::<usize>();
104        let average_complexity = if total_functions > 0 {
105            all_functions.iter().map(|f| f.complexity).sum::<f64>()
106                / total_functions as f64
107        } else {
108            0.0
109        };
110        let largest_function = all_functions.iter().max_by_key(|f| f.lines).cloned();
111        Ok(CodeAnalysis {
112            total_functions,
113            public_functions,
114            async_functions,
115            average_complexity,
116            total_lines,
117            largest_function,
118            functions: all_functions,
119        })
120    }
121    fn display_analysis(&self, analysis: &CodeAnalysis, verbose: bool) {
122        println!(
123            "\nšŸ“Š {} - Code Analysis Report", "CargoMate CodeAnalyzer".bold().blue()
124        );
125        println!("{}", "═".repeat(50).blue());
126        println!("\nšŸ“ˆ Summary:");
127        println!("  • Total Functions: {}", analysis.total_functions);
128        println!("  • Public API: {}", analysis.public_functions);
129        println!("  • Async Functions: {}", analysis.async_functions);
130        println!("  • Total Lines: {}", analysis.total_lines);
131        println!("  • Average Complexity: {:.2}", analysis.average_complexity);
132        if let Some(ref largest) = analysis.largest_function {
133            println!(
134                "  • Largest Function: {} ({} lines)", largest.name, largest.lines
135            );
136        }
137        if verbose && !analysis.functions.is_empty() {
138            println!("\nšŸ” Function Details:");
139            for function in &analysis.functions {
140                let complexity_color = if function.complexity > 10.0 {
141                    format!("{:.2}", function.complexity).red()
142                } else if function.complexity > 5.0 {
143                    format!("{:.2}", function.complexity).yellow()
144                } else {
145                    format!("{:.2}", function.complexity).green()
146                };
147                println!(
148                    "  • {}::{} ({}, {} lines, complexity: {})", function.file
149                    .split('/').last().unwrap_or(& function.file), function.name.cyan(),
150                    if function.is_public { "public".green() } else { "private".dimmed()
151                    }, function.lines, complexity_color
152                );
153            }
154        }
155        println!("\nāœ… Analysis complete!");
156    }
157}
158impl Tool for CodeAnalyzer {
159    fn name(&self) -> &'static str {
160        "code-analyzer"
161    }
162    fn description(&self) -> &'static str {
163        "Analyze Rust code metrics and complexity"
164    }
165    fn command(&self) -> Command {
166        Command::new(self.name())
167            .about(self.description())
168            .long_about(
169                "Analyzes Rust code to provide metrics on function complexity, API surface, and code quality indicators.",
170            )
171            .args(
172                &[
173                    Arg::new("path")
174                        .long("path")
175                        .short('p')
176                        .help("Path to Rust project to analyze")
177                        .default_value("."),
178                    Arg::new("output-format")
179                        .long("output-format")
180                        .short('f')
181                        .help("Output format (human, json, table)")
182                        .default_value("human"),
183                ],
184            )
185            .args(&common_options())
186    }
187    fn execute(&self, matches: &ArgMatches) -> Result<()> {
188        let path = matches.get_one::<String>("path").unwrap();
189        let output_format = matches.get_one::<String>("output-format").unwrap();
190        let verbose = matches.get_flag("verbose");
191        println!(
192            "šŸ“Š {} - Analyzing Code Metrics", "CargoMate CodeAnalyzer".bold().blue()
193        );
194        if !Path::new(path).exists() {
195            return Err(
196                ToolError::InvalidArguments(format!("Path {} does not exist", path)),
197            );
198        }
199        let analysis = self.analyze_codebase(path)?;
200        if analysis.total_functions == 0 {
201            println!("{}", "No Rust functions found to analyze".yellow());
202            return Ok(());
203        }
204        match output_format.as_str() {
205            "json" => {
206                let json = serde_json::to_string_pretty(&analysis)?;
207                println!("{}", json);
208            }
209            "table" => {
210                println!(
211                    "{:<30} {:<10} {:<12} {:<8} {:<8}", "Function", "Lines",
212                    "Complexity", "Params", "Public"
213                );
214                println!("{}", "─".repeat(70));
215                for function in &analysis.functions {
216                    println!(
217                        "{:<30} {:<10} {:<12.2} {:<8} {:<8}", format!("{}::{}", function
218                        .file.split('/').last().unwrap_or(""), function.name), function
219                        .lines, function.complexity, function.parameters, if function
220                        .is_public { "Yes" } else { "No" }
221                    );
222                }
223            }
224            _ => {
225                self.display_analysis(&analysis, verbose);
226            }
227        }
228        Ok(())
229    }
230}
231impl Default for CodeAnalyzer {
232    fn default() -> Self {
233        Self::new()
234    }
235}