Skip to main content

cargo_mate/
smart_parser.rs

1use anyhow::{Context, Result};
2use colored::*;
3use regex::Regex;
4use reqwest;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9#[derive(Debug, Clone)]
10pub struct SmartError {
11    pub code: String,
12    pub message: String,
13    pub file: PathBuf,
14    pub line: usize,
15    pub column: usize,
16    pub suggestion: Option<String>,
17    pub fix_command: Option<String>,
18    pub explanation: Option<String>,
19    pub related_docs: Vec<String>,
20}
21#[derive(Debug, Serialize, Deserialize)]
22struct ErrorPattern {
23    code: String,
24    pattern: String,
25    suggestion: String,
26    fix_command: Option<String>,
27    docs_url: Option<String>,
28}
29#[derive(Debug, Serialize, Deserialize)]
30struct ErrorDatabase {
31    patterns: Vec<ErrorPattern>,
32    common_fixes: HashMap<String, Vec<String>>,
33    learning_data: HashMap<String, FixHistory>,
34}
35#[derive(Debug, Serialize, Deserialize)]
36struct FixHistory {
37    successful_fixes: Vec<String>,
38    failed_attempts: Vec<String>,
39    frequency: usize,
40}
41pub struct SmartParser {
42    error_db: ErrorDatabase,
43    db_file: PathBuf,
44    online_search: bool,
45}
46impl SmartParser {
47    pub fn new() -> Result<Self> {
48        let db_file = dirs::home_dir()
49            .context("Could not find home directory")?
50            .join(".shipwreck")
51            .join("error_db.json");
52        let error_db = if db_file.exists() {
53            let content = fs::read_to_string(&db_file)?;
54            serde_json::from_str(&content)?
55        } else {
56            Self::create_default_database()
57        };
58        Ok(Self {
59            error_db,
60            db_file,
61            online_search: true,
62        })
63    }
64    fn create_default_database() -> ErrorDatabase {
65        let patterns = vec![
66            ErrorPattern { code : "E0308".to_string(), pattern : "mismatched types"
67            .to_string(), suggestion :
68            "Check the expected and found types. You may need to convert types using methods like .to_string(), .into(), or as_ref()"
69            .to_string(), fix_command : Some("cargo fix --allow-dirty".to_string()),
70            docs_url : Some("https://doc.rust-lang.org/error-index.html#E0308"
71            .to_string()), }, ErrorPattern { code : "E0382".to_string(), pattern :
72            "borrow of moved value".to_string(), suggestion :
73            "The value has been moved. Consider cloning it, using a reference, or restructuring ownership"
74            .to_string(), fix_command : None, docs_url :
75            Some("https://doc.rust-lang.org/error-index.html#E0382".to_string()), },
76            ErrorPattern { code : "E0499".to_string(), pattern :
77            "cannot borrow .* as mutable more than once".to_string(), suggestion :
78            "You have multiple mutable borrows. Consider using RefCell for interior mutability or restructuring your code"
79            .to_string(), fix_command : None, docs_url :
80            Some("https://doc.rust-lang.org/error-index.html#E0499".to_string()), },
81            ErrorPattern { code : "E0277".to_string(), pattern :
82            "the trait bound .* is not satisfied".to_string(), suggestion :
83            "The required trait is not implemented. Check if you need to import the trait or implement it for your type"
84            .to_string(), fix_command : None, docs_url :
85            Some("https://doc.rust-lang.org/error-index.html#E0277".to_string()), },
86            ErrorPattern { code : "E0433".to_string(), pattern : "failed to resolve"
87            .to_string(), suggestion :
88            "Module or item not found. Check your imports and module declarations"
89            .to_string(), fix_command : Some("cargo check".to_string()), docs_url :
90            Some("https://doc.rust-lang.org/error-index.html#E0433".to_string()), },
91        ];
92        let mut common_fixes = HashMap::new();
93        common_fixes
94            .insert(
95                "lifetime".to_string(),
96                vec![
97                    "Add lifetime parameters to your struct/function".to_string(),
98                    "Use 'static lifetime if the value needs to live for the entire program"
99                    .to_string(), "Consider using Arc<T> or Rc<T> for shared ownership"
100                    .to_string(),
101                ],
102            );
103        common_fixes
104            .insert(
105                "async".to_string(),
106                vec![
107                    "Make sure you're using .await on async functions".to_string(),
108                    "Check if your function needs to be async".to_string(),
109                    "Consider using tokio::spawn or async blocks".to_string(),
110                ],
111            );
112        common_fixes
113            .insert(
114                "iterator".to_string(),
115                vec![
116                    "Check if you need to call .collect() to consume the iterator"
117                    .to_string(),
118                    "Make sure you're using the right iterator method (map, filter, fold, etc.)"
119                    .to_string(),
120                    "Consider using .iter() or .into_iter() based on ownership needs"
121                    .to_string(),
122                ],
123            );
124        ErrorDatabase {
125            patterns,
126            common_fixes,
127            learning_data: HashMap::new(),
128        }
129    }
130    pub fn parse_error(&mut self, error_text: &str) -> SmartError {
131        let code = self.extract_error_code(error_text);
132        let (file, line, column) = self.extract_location(error_text);
133        let message = self.extract_message(error_text);
134        let suggestion = self.get_suggestion(&code, &message);
135        let fix_command = self.get_fix_command(&code);
136        let explanation = self.get_explanation(&code, &message);
137        let related_docs = self.get_related_docs(&code);
138        self.learn_from_error(&code, &message);
139        SmartError {
140            code,
141            message,
142            file,
143            line,
144            column,
145            suggestion,
146            fix_command,
147            explanation,
148            related_docs,
149        }
150    }
151    fn extract_error_code(&self, text: &str) -> String {
152        let re = Regex::new(r"error\[([A-Z0-9]+)\]").unwrap();
153        re.captures(text)
154            .and_then(|cap| cap.get(1))
155            .map(|m| m.as_str().to_string())
156            .unwrap_or_else(|| "unknown".to_string())
157    }
158    fn extract_location(&self, text: &str) -> (PathBuf, usize, usize) {
159        let re = Regex::new(r"(\S+\.rs):(\d+):(\d+)").unwrap();
160        if let Some(cap) = re.captures(text) {
161            let file = PathBuf::from(cap.get(1).unwrap().as_str());
162            let line = cap.get(2).unwrap().as_str().parse().unwrap_or(0);
163            let column = cap.get(3).unwrap().as_str().parse().unwrap_or(0);
164            (file, line, column)
165        } else {
166            (PathBuf::from("unknown"), 0, 0)
167        }
168    }
169    fn extract_message(&self, text: &str) -> String {
170        let lines: Vec<&str> = text.lines().collect();
171        if let Some(error_line) = lines.iter().find(|l| l.contains("error")) {
172            let parts: Vec<&str> = error_line.split(':').collect();
173            if parts.len() > 1 {
174                return parts[1..].join(":").trim().to_string();
175            }
176        }
177        text.to_string()
178    }
179    fn get_suggestion(&self, code: &str, message: &str) -> Option<String> {
180        for pattern in &self.error_db.patterns {
181            if pattern.code == code {
182                return Some(pattern.suggestion.clone());
183            }
184        }
185        if message.contains("lifetime") {
186            if let Some(fixes) = self.error_db.common_fixes.get("lifetime") {
187                return Some(fixes.join("\n"));
188            }
189        }
190        if message.contains("async") || message.contains("await") {
191            if let Some(fixes) = self.error_db.common_fixes.get("async") {
192                return Some(fixes.join("\n"));
193            }
194        }
195        if self.online_search {
196            self.search_online_suggestion(code, message).ok()
197        } else {
198            None
199        }
200    }
201    fn get_fix_command(&self, code: &str) -> Option<String> {
202        self.error_db
203            .patterns
204            .iter()
205            .find(|p| p.code == code)
206            .and_then(|p| p.fix_command.clone())
207    }
208    fn get_explanation(&self, code: &str, message: &str) -> Option<String> {
209        let base_explanation = match code {
210            "E0308" => {
211                "Type mismatch occurs when Rust expects one type but finds another."
212            }
213            "E0382" => {
214                "Ownership has been transferred. In Rust, each value has a single owner."
215            }
216            "E0499" => {
217                "Rust's borrowing rules prevent multiple mutable references to prevent data races."
218            }
219            "E0277" => {
220                "A trait bound was not satisfied. The type doesn't implement the required trait."
221            }
222            "E0433" => "Failed to resolve a path to a module, type, or function.",
223            _ => return None,
224        };
225        Some(format!("{}\n\nContext: {}", base_explanation, message))
226    }
227    fn get_related_docs(&self, code: &str) -> Vec<String> {
228        let mut docs = vec![
229            format!("https://doc.rust-lang.org/error-index.html#{}", code),
230        ];
231        if let Some(pattern) = self.error_db.patterns.iter().find(|p| p.code == code) {
232            if let Some(ref url) = pattern.docs_url {
233                docs.push(url.clone());
234            }
235        }
236        docs.push("https://doc.rust-lang.org/book/".to_string());
237        docs
238    }
239    fn search_online_suggestion(&self, code: &str, message: &str) -> Result<String> {
240        Ok(
241            format!(
242                "šŸ’” Search for solutions:\n  - Stack Overflow: rust {} {}\n  - Rust Forum: https://users.rust-lang.org/",
243                code, message.split_whitespace().take(5).collect::< Vec < _ >> ()
244                .join(" ")
245            ),
246        )
247    }
248    fn learn_from_error(&mut self, code: &str, _message: &str) {
249        let entry = self
250            .error_db
251            .learning_data
252            .entry(code.to_string())
253            .or_insert(FixHistory {
254                successful_fixes: Vec::new(),
255                failed_attempts: Vec::new(),
256                frequency: 0,
257            });
258        entry.frequency += 1;
259        let _ = self.save_database();
260    }
261    pub fn record_fix(&mut self, code: &str, fix: &str, successful: bool) -> Result<()> {
262        let entry = self
263            .error_db
264            .learning_data
265            .entry(code.to_string())
266            .or_insert(FixHistory {
267                successful_fixes: Vec::new(),
268                failed_attempts: Vec::new(),
269                frequency: 0,
270            });
271        if successful {
272            entry.successful_fixes.push(fix.to_string());
273        } else {
274            entry.failed_attempts.push(fix.to_string());
275        }
276        self.save_database()?;
277        Ok(())
278    }
279    pub fn display_smart_error(&self, error: &SmartError) {
280        println!(
281            "{}", format!("═══ Error {} ═══", error.code) .red().bold()
282        );
283        println!(
284            "šŸ“ {}:{}:{}", error.file.display(), error.line.to_string().yellow(), error
285            .column
286        );
287        println!("šŸ“ {}", error.message.white());
288        if let Some(ref suggestion) = error.suggestion {
289            println!("\nšŸ’” {}", "Suggestion:".green().bold());
290            for line in suggestion.lines() {
291                println!("   {}", line);
292            }
293        }
294        if let Some(ref cmd) = error.fix_command {
295            println!("\nšŸ”§ {}", "Quick fix:".cyan().bold());
296            println!("   {}", cmd.cyan());
297        }
298        if let Some(ref explanation) = error.explanation {
299            println!("\nšŸ“– {}", "Explanation:".blue().bold());
300            for line in explanation.lines() {
301                println!("   {}", line.dimmed());
302            }
303        }
304        if !error.related_docs.is_empty() {
305            println!("\nšŸ“š {}", "Related Documentation:".magenta().bold());
306            for doc in &error.related_docs {
307                println!("   • {}", doc.underline());
308            }
309        }
310        println!("{}", "═".repeat(50).red());
311    }
312    pub fn suggest_learning_path(&self, errors: &[SmartError]) {
313        let mut error_categories: HashMap<String, usize> = HashMap::new();
314        for error in errors {
315            let category = self.categorize_error(&error.code);
316            *error_categories.entry(category).or_insert(0) += 1;
317        }
318        if error_categories.is_empty() {
319            return;
320        }
321        println!("{}", "šŸ“š Learning Path Recommendation".green().bold());
322        println!("Based on your errors, consider studying:");
323        let mut categories: Vec<_> = error_categories.into_iter().collect();
324        categories.sort_by(|a, b| b.1.cmp(&a.1));
325        for (category, count) in categories.iter().take(3) {
326            let resources = self.get_learning_resources(category);
327            println!("\n{} ({} errors)", category.cyan(), count);
328            for resource in resources {
329                println!("  • {}", resource);
330            }
331        }
332    }
333    fn categorize_error(&self, code: &str) -> String {
334        match code {
335            c if c.starts_with("E03") => "Ownership & Borrowing".to_string(),
336            c if c.starts_with("E04") => "Pattern Matching".to_string(),
337            c if c.starts_with("E05") => "Traits & Generics".to_string(),
338            c if c.starts_with("E06") => "Modules & Visibility".to_string(),
339            c if c.starts_with("E07") => "Async & Concurrency".to_string(),
340            _ => "General Rust".to_string(),
341        }
342    }
343    fn get_learning_resources(&self, category: &str) -> Vec<String> {
344        match category {
345            "Ownership & Borrowing" => {
346                vec![
347                    "The Rust Book - Chapter 4: Understanding Ownership".to_string(),
348                    "Rust by Example - Ownership section".to_string(),
349                    "Video: 'Rust Ownership Explained' by No Boilerplate".to_string(),
350                ]
351            }
352            "Traits & Generics" => {
353                vec![
354                    "The Rust Book - Chapter 10: Generic Types, Traits".to_string(),
355                    "Rust by Example - Traits section".to_string(),
356                    "Blog: 'Rust Traits: A Deep Dive'".to_string(),
357                ]
358            }
359            _ => {
360                vec![
361                    "The Rust Programming Language Book".to_string(), "Rust by Example"
362                    .to_string(), "Rustlings exercises".to_string(),
363                ]
364            }
365        }
366    }
367    fn save_database(&self) -> Result<()> {
368        let json = serde_json::to_string_pretty(&self.error_db)?;
369        fs::write(&self.db_file, json)?;
370        Ok(())
371    }
372}