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}