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}