Skip to main content

cargo_mate/tools/
panic_analyzer.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::fs;
6use std::collections::HashMap;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9#[derive(Debug, Clone)]
10pub struct PanicAnalyzerTool;
11#[derive(Debug, Clone, Deserialize, Serialize)]
12struct PanicInfo {
13    message: String,
14    location: String,
15    context: Vec<String>,
16    suggestions: Vec<String>,
17    timestamp: String,
18    frequency: usize,
19}
20#[derive(Debug, Deserialize, Serialize)]
21struct PanicReport {
22    total_panics: usize,
23    unique_patterns: usize,
24    most_common: Vec<PanicInfo>,
25    recent_panics: Vec<PanicInfo>,
26}
27impl PanicAnalyzerTool {
28    pub fn new() -> Self {
29        Self
30    }
31    fn parse_panic_message(&self, line: &str) -> Option<String> {
32        let panic_patterns = [
33            r"thread '.*' panicked at '(.*?)'",
34            r"panic!(.*)",
35            r"unreachable!(.*)",
36            r"todo!(.*)",
37            r"unimplemented!(.*)",
38        ];
39        for pattern in &panic_patterns {
40            if let Ok(regex) = Regex::new(pattern) {
41                if let Some(captures) = regex.captures(line) {
42                    if let Some(message) = captures.get(1) {
43                        return Some(message.as_str().to_string());
44                    }
45                }
46            }
47        }
48        None
49    }
50    fn parse_location(&self, line: &str) -> Option<String> {
51        let location_patterns = [r"at (.+:\d+)", r"at (.+:\d+:\d+)"];
52        for pattern in &location_patterns {
53            if let Ok(regex) = Regex::new(pattern) {
54                if let Some(captures) = regex.captures(line) {
55                    if let Some(location) = captures.get(1) {
56                        return Some(location.as_str().to_string());
57                    }
58                }
59            }
60        }
61        None
62    }
63    fn extract_source_context(
64        &self,
65        location: &str,
66        context_lines: usize,
67    ) -> Result<Vec<String>> {
68        let parts: Vec<&str> = location.split(':').collect();
69        if parts.len() < 2 {
70            return Ok(vec!["Could not parse location".to_string()]);
71        }
72        let file_path = parts[0];
73        let line_num: usize = parts[1].parse().unwrap_or(1);
74        let path = Path::new(file_path);
75        if !path.exists() {
76            return Ok(vec![format!("File not found: {}", file_path)]);
77        }
78        let content = fs::read_to_string(path)
79            .map_err(|e| ToolError::ExecutionFailed(
80                format!("Cannot read {}: {}", file_path, e),
81            ))?;
82        let lines: Vec<&str> = content.lines().collect();
83        let start = line_num.saturating_sub(context_lines + 1);
84        let end = (line_num + context_lines).min(lines.len());
85        let mut context = Vec::new();
86        for (i, line) in lines.iter().enumerate().skip(start).take(end - start) {
87            let marker = if i + 1 == line_num { ">>> " } else { "    " };
88            context.push(format!("{}{}: {}", marker, i + 1, line));
89        }
90        Ok(context)
91    }
92    fn generate_suggestions(&self, panic_message: &str) -> Vec<String> {
93        let mut suggestions = Vec::new();
94        let message_lower = panic_message.to_lowercase();
95        if message_lower.contains("index out of bounds") {
96            suggestions
97                .push("Check array/vector bounds before accessing elements".to_string());
98            suggestions
99                .push("Use .get(index) instead of [index] for safe access".to_string());
100            suggestions.push("Add bounds checking with if index < len".to_string());
101        }
102        if message_lower.contains("called `option::unwrap()`") {
103            suggestions
104                .push("Use .unwrap_or(default_value) for safe unwrapping".to_string());
105            suggestions
106                .push(
107                    "Use .unwrap_or_else(|| default_fn()) for computed defaults"
108                        .to_string(),
109                );
110            suggestions.push("Use if let Some(value) = option pattern".to_string());
111        }
112        if message_lower.contains("called `result::unwrap()`") {
113            suggestions
114                .push(
115                    "Use .unwrap_or(default_value) for safe error handling".to_string(),
116                );
117            suggestions
118                .push("Use ? operator in functions that return Result".to_string());
119            suggestions
120                .push("Use match or if let for proper error handling".to_string());
121        }
122        if message_lower.contains("borrow checker") {
123            suggestions
124                .push(
125                    "Check for multiple mutable borrows of the same value".to_string(),
126                );
127            suggestions.push("Use references with different lifetimes".to_string());
128            suggestions.push("Consider cloning the value if appropriate".to_string());
129        }
130        if message_lower.contains("cannot move out") {
131            suggestions.push("Use references (&) instead of moving values".to_string());
132            suggestions.push("Implement Copy trait for simple types".to_string());
133            suggestions.push("Use .clone() if the type implements Clone".to_string());
134        }
135        if message_lower.contains("overflow") {
136            suggestions
137                .push(
138                    "Use checked operations: checked_add, checked_sub, etc.".to_string(),
139                );
140            suggestions
141                .push("Add bounds checking before arithmetic operations".to_string());
142            suggestions
143                .push(
144                    "Use saturating operations for safe overflow handling".to_string(),
145                );
146        }
147        if suggestions.is_empty() {
148            suggestions
149                .push(
150                    "Review the panic location and ensure proper error handling"
151                        .to_string(),
152                );
153            suggestions
154                .push("Consider using Result<T, E> instead of panicking".to_string());
155            suggestions.push("Add debug logging before the panic location".to_string());
156        }
157        suggestions
158    }
159    fn analyze_log_file(&self, log_path: &str) -> Result<Vec<PanicInfo>> {
160        let path = Path::new(log_path);
161        if !path.exists() {
162            return Err(
163                ToolError::ExecutionFailed(format!("Log file not found: {}", log_path)),
164            );
165        }
166        let content = fs::read_to_string(path)
167            .map_err(|e| ToolError::ExecutionFailed(
168                format!("Cannot read log file: {}", e),
169            ))?;
170        let mut panics = Vec::new();
171        let lines: Vec<&str> = content.lines().collect();
172        for (i, line) in lines.iter().enumerate() {
173            if let Some(message) = self.parse_panic_message(line) {
174                let location = if let Some(loc) = self.parse_location(line) {
175                    loc
176                } else {
177                    let mut location = "Unknown location".to_string();
178                    for j in (i + 1)..lines.len().min(i + 10) {
179                        if let Some(loc) = self.parse_location(lines[j]) {
180                            location = loc;
181                            break;
182                        }
183                    }
184                    location
185                };
186                let context = if location != "Unknown location" {
187                    self.extract_source_context(&location, 3).unwrap_or_default()
188                } else {
189                    vec!["Could not extract source context".to_string()]
190                };
191                let suggestions = self.generate_suggestions(&message);
192                panics
193                    .push(PanicInfo {
194                        message,
195                        location,
196                        context,
197                        suggestions,
198                        timestamp: chrono::Utc::now().to_rfc3339(),
199                        frequency: 1,
200                    });
201            }
202        }
203        Ok(panics)
204    }
205    fn analyze_recent_panics(&self, count: usize) -> Result<Vec<PanicInfo>> {
206        let log_paths = [
207            ".cargo-mate/panics.log",
208            "target/debug/panic.log",
209            "/tmp/cargo-mate-panics.log",
210        ];
211        let mut all_panics = Vec::new();
212        for log_path in &log_paths {
213            if let Ok(panics) = self.analyze_log_file(log_path) {
214                all_panics.extend(panics);
215            }
216        }
217        all_panics.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
218        all_panics.truncate(count);
219        Ok(all_panics)
220    }
221    fn group_similar_panics(
222        &self,
223        panics: &[PanicInfo],
224    ) -> HashMap<String, Vec<PanicInfo>> {
225        let mut groups: HashMap<String, Vec<PanicInfo>> = HashMap::new();
226        for panic in panics {
227            let key = panic.message.to_lowercase();
228            groups.entry(key).or_insert_with(Vec::new).push(panic.clone());
229        }
230        groups
231    }
232    fn generate_report(
233        &self,
234        panics: &[PanicInfo],
235        format: OutputFormat,
236        verbose: bool,
237    ) -> Result<()> {
238        match format {
239            OutputFormat::Json => {
240                let grouped = self.group_similar_panics(panics);
241                let report = PanicReport {
242                    total_panics: panics.len(),
243                    unique_patterns: grouped.len(),
244                    most_common: panics.to_vec(),
245                    recent_panics: panics.to_vec(),
246                };
247                println!("{}", serde_json::to_string_pretty(& report).unwrap());
248            }
249            OutputFormat::Table => {
250                println!(
251                    "{:<50} {:<30} {:<15}", "Panic Message", "Location", "Suggestions"
252                );
253                println!("{}", "─".repeat(100));
254                for panic in panics {
255                    let message = panic.message.chars().take(47).collect::<String>();
256                    let location = panic.location.chars().take(27).collect::<String>();
257                    let suggestion_count = panic.suggestions.len().to_string();
258                    println!(
259                        "{:<50} {:<30} {:<15}", message, location,
260                        format!("{} suggestions", suggestion_count)
261                    );
262                }
263            }
264            OutputFormat::Human => {
265                println!("{}", "🚨 Panic Analysis Report".bold().red());
266                println!("{}", "═".repeat(50).red());
267                if panics.is_empty() {
268                    println!("āœ… No panics found in recent logs");
269                    return Ok(());
270                }
271                println!("šŸ“Š Found {} panic(s)", panics.len());
272                let grouped = self.group_similar_panics(panics);
273                println!("šŸ” Unique patterns: {}", grouped.len());
274                for (i, panic) in panics.iter().enumerate() {
275                    println!("\n{}. {}", i + 1, panic.message.red().bold());
276                    println!("   šŸ“ Location: {}", panic.location.cyan());
277                    if verbose {
278                        println!("   šŸ“ Context:");
279                        for line in &panic.context {
280                            println!("      {}", line);
281                        }
282                        if !panic.suggestions.is_empty() {
283                            println!("   šŸ’” Suggestions:");
284                            for suggestion in &panic.suggestions {
285                                println!("      • {}", suggestion.yellow());
286                            }
287                        }
288                    }
289                }
290                if !verbose && !panics.is_empty() {
291                    println!(
292                        "\nšŸ’” Use --verbose to see source context and fix suggestions"
293                    );
294                }
295            }
296        }
297        Ok(())
298    }
299}
300impl Tool for PanicAnalyzerTool {
301    fn name(&self) -> &'static str {
302        "panic-analyzer"
303    }
304    fn description(&self) -> &'static str {
305        "Parse panic messages and provide debugging context with fix suggestions"
306    }
307    fn command(&self) -> Command {
308        Command::new(self.name())
309            .about(self.description())
310            .long_about(
311                "Analyze panic messages from logs, show source code context, and provide fix suggestions based on common patterns",
312            )
313            .args(
314                &[
315                    Arg::new("recent")
316                        .long("recent")
317                        .short('r')
318                        .help("Analyze recent panics from log files")
319                        .action(clap::ArgAction::SetTrue),
320                    Arg::new("count")
321                        .long("count")
322                        .short('c')
323                        .help("Number of recent panics to analyze")
324                        .default_value("10"),
325                    Arg::new("log-file")
326                        .long("log-file")
327                        .short('f')
328                        .help("Specific log file to analyze"),
329                    Arg::new("pattern")
330                        .long("pattern")
331                        .short('p')
332                        .help("Search for specific panic patterns"),
333                    Arg::new("context")
334                        .long("context")
335                        .short('x')
336                        .help("Number of context lines around panic location")
337                        .default_value("3"),
338                    Arg::new("suggest-fixes")
339                        .long("suggest-fixes")
340                        .help("Show fix suggestions for panics")
341                        .action(clap::ArgAction::SetTrue),
342                    Arg::new("report")
343                        .long("report")
344                        .help("Generate panic analysis report")
345                        .action(clap::ArgAction::SetTrue),
346                ],
347            )
348            .args(&common_options())
349    }
350    fn execute(&self, matches: &ArgMatches) -> Result<()> {
351        let recent = matches.get_flag("recent");
352        let count: usize = matches
353            .get_one::<String>("count")
354            .unwrap()
355            .parse()
356            .map_err(|_| ToolError::InvalidArguments(
357                "Invalid count value".to_string(),
358            ))?;
359        let log_file = matches.get_one::<String>("log-file");
360        let pattern = matches.get_one::<String>("pattern");
361        let context_lines: usize = matches
362            .get_one::<String>("context")
363            .unwrap()
364            .parse()
365            .map_err(|_| ToolError::InvalidArguments(
366                "Invalid context value".to_string(),
367            ))?;
368        let suggest_fixes = matches.get_flag("suggest-fixes");
369        let report = matches.get_flag("report");
370        let output_format = parse_output_format(matches);
371        let verbose = matches.get_flag("verbose");
372        println!(
373            "🚨 {} - Analyzing panic messages", "CargoMate PanicAnalyzer".bold().red()
374        );
375        let panics = if let Some(log_path) = log_file {
376            self.analyze_log_file(log_path)?
377        } else if recent {
378            self.analyze_recent_panics(count)?
379        } else {
380            self.analyze_recent_panics(count)?
381        };
382        let filtered_panics: Vec<PanicInfo> = if let Some(pat) = pattern {
383            panics
384                .into_iter()
385                .filter(|p| p.message.to_lowercase().contains(&pat.to_lowercase()))
386                .collect()
387        } else {
388            panics
389        };
390        if filtered_panics.is_empty() {
391            println!("āœ… No panics found matching criteria");
392            return Ok(());
393        }
394        if report {
395            self.generate_report(&filtered_panics, output_format, verbose)?;
396        } else {
397            self.generate_report(&filtered_panics, output_format, verbose)?;
398        }
399        Ok(())
400    }
401}
402impl Default for PanicAnalyzerTool {
403    fn default() -> Self {
404        Self::new()
405    }
406}