1use crate::config::HealingConfig;
10use crate::{PawanError, Result};
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13use std::process::Stdio;
14use tokio::io::AsyncReadExt;
15use tokio::process::Command;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Diagnostic {
20 pub kind: DiagnosticKind,
22 pub message: String,
24 pub file: Option<PathBuf>,
26 pub line: Option<usize>,
28 pub column: Option<usize>,
30 pub code: Option<String>,
32 pub suggestion: Option<String>,
34 pub raw: String,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum DiagnosticKind {
41 Error,
42 Warning,
43 Note,
44 Help,
45}
46
47#[derive(Debug)]
49pub struct HealingResult {
56 pub remaining: Vec<Diagnostic>,
58 pub summary: String,
60}
61
62pub struct CompilerFixer {
69 workspace_root: PathBuf,
70}
71
72impl CompilerFixer {
73 pub fn new(workspace_root: PathBuf) -> Self {
75 Self { workspace_root }
76 }
77
78 pub fn parse_diagnostics(&self, output: &str) -> Vec<Diagnostic> {
89 let mut diagnostics = Vec::new();
90
91 for line in output.lines() {
93 if let Ok(json) = serde_json::from_str::<serde_json::Value>(line) {
94 if let Some(msg) = json.get("message") {
95 let diagnostic = self.parse_diagnostic_message(msg, line);
96 if let Some(d) = diagnostic {
97 diagnostics.push(d);
98 }
99 }
100 }
101 }
102
103 if diagnostics.is_empty() {
105 diagnostics = self.parse_text_diagnostics(output);
106 }
107
108 diagnostics
109 }
110
111 fn parse_diagnostic_message(&self, msg: &serde_json::Value, raw: &str) -> Option<Diagnostic> {
113 let level = msg.get("level")?.as_str()?;
114 let message = msg.get("message")?.as_str()?.to_string();
115
116 let kind = match level {
117 "error" => DiagnosticKind::Error,
118 "warning" => DiagnosticKind::Warning,
119 "note" => DiagnosticKind::Note,
120 "help" => DiagnosticKind::Help,
121 _ => return None,
122 };
123
124 if message.contains("internal compiler error") {
126 return None;
127 }
128
129 let code = msg
131 .get("code")
132 .and_then(|c| c.get("code"))
133 .and_then(|c| c.as_str())
134 .map(|s| s.to_string());
135
136 let spans = msg.get("spans")?.as_array()?;
138 let primary_span = spans.iter().find(|s| {
139 s.get("is_primary")
140 .and_then(|v| v.as_bool())
141 .unwrap_or(false)
142 });
143
144 let (file, line, column) = if let Some(span) = primary_span {
145 let file = span
146 .get("file_name")
147 .and_then(|v| v.as_str())
148 .map(PathBuf::from);
149 let line = span
150 .get("line_start")
151 .and_then(|v| v.as_u64())
152 .map(|v| v as usize);
153 let column = span
154 .get("column_start")
155 .and_then(|v| v.as_u64())
156 .map(|v| v as usize);
157 (file, line, column)
158 } else {
159 (None, None, None)
160 };
161
162 let suggestion = msg
164 .get("children")
165 .and_then(|c| c.as_array())
166 .and_then(|children| {
167 children.iter().find_map(|child| {
168 let level = child.get("level")?.as_str()?;
169 if level == "help" {
170 let help_msg = child.get("message")?.as_str()?;
171 if let Some(spans) = child.get("spans").and_then(|s| s.as_array()) {
173 for span in spans {
174 if let Some(replacement) =
175 span.get("suggested_replacement").and_then(|v| v.as_str())
176 {
177 return Some(format!("{}: {}", help_msg, replacement));
178 }
179 }
180 }
181 return Some(help_msg.to_string());
182 }
183 None
184 })
185 });
186
187 Some(Diagnostic {
188 kind,
189 message,
190 file,
191 line,
192 column,
193 code,
194 suggestion,
195 raw: raw.to_string(),
196 })
197 }
198
199 fn parse_text_diagnostics(&self, output: &str) -> Vec<Diagnostic> {
201 let mut diagnostics = Vec::new();
202 let mut current_diagnostic: Option<Diagnostic> = None;
203
204 for line in output.lines() {
205 if line.starts_with("error") || line.starts_with("warning") {
207 if let Some(d) = current_diagnostic.take() {
209 diagnostics.push(d);
210 }
211
212 let kind = if line.starts_with("error") {
213 DiagnosticKind::Error
214 } else {
215 DiagnosticKind::Warning
216 };
217
218 let code = line
220 .find('[')
221 .and_then(|start| line.find(']').map(|end| line[start + 1..end].to_string()));
222
223 let message = if let Some(colon_pos) = line.find("]: ") {
225 line[colon_pos + 3..].to_string()
226 } else if let Some(colon_pos) = line.find(": ") {
227 line[colon_pos + 2..].to_string()
228 } else {
229 line.to_string()
230 };
231
232 current_diagnostic = Some(Diagnostic {
233 kind,
234 message,
235 file: None,
236 line: None,
237 column: None,
238 code,
239 suggestion: None,
240 raw: line.to_string(),
241 });
242 }
243 else if line.trim().starts_with("-->") {
245 if let Some(ref mut d) = current_diagnostic {
246 let path_part = line.trim().trim_start_matches("-->").trim();
247 let parts: Vec<&str> = path_part.rsplitn(3, ':').collect();
249 if parts.len() >= 2 {
250 d.column = parts[0].parse().ok();
251 d.line = parts[1].parse().ok();
252 if parts.len() >= 3 {
253 d.file = Some(PathBuf::from(parts[2]));
254 }
255 }
256 }
257 }
258 else if line.trim().starts_with("help:") {
260 if let Some(ref mut d) = current_diagnostic {
261 let suggestion = line.trim().trim_start_matches("help:").trim();
262 d.suggestion = Some(suggestion.to_string());
263 }
264 }
265 }
266
267 if let Some(d) = current_diagnostic {
269 diagnostics.push(d);
270 }
271
272 diagnostics
273 }
274
275 pub async fn check(&self) -> Result<Vec<Diagnostic>> {
277 let output = self.run_cargo(&["check", "--message-format=json"]).await?;
278 Ok(self.parse_diagnostics(&output))
279 }
280
281 async fn run_cargo(&self, args: &[&str]) -> Result<String> {
283 let mut cmd = Command::new("cargo");
284 cmd.args(args)
285 .current_dir(&self.workspace_root)
286 .stdout(Stdio::piped())
287 .stderr(Stdio::piped())
288 .stdin(Stdio::null());
289
290 let mut child = cmd.spawn().map_err(PawanError::Io)?;
291
292 let mut stdout = String::new();
293 let mut stderr = String::new();
294
295 if let Some(mut handle) = child.stdout.take() {
296 handle.read_to_string(&mut stdout).await.ok();
297 }
298
299 if let Some(mut handle) = child.stderr.take() {
300 handle.read_to_string(&mut stderr).await.ok();
301 }
302
303 child.wait().await.map_err(PawanError::Io)?;
304
305 Ok(format!("{}\n{}", stdout, stderr))
307 }
308}
309
310pub struct ClippyFixer {
322 workspace_root: PathBuf,
323}
324
325impl ClippyFixer {
326 pub fn new(workspace_root: PathBuf) -> Self {
328 Self { workspace_root }
329 }
330
331 pub async fn check(&self) -> Result<Vec<Diagnostic>> {
333 let output = self.run_clippy().await?;
334 let fixer = CompilerFixer::new(self.workspace_root.clone());
335 let mut diagnostics = fixer.parse_diagnostics(&output);
336
337 diagnostics.retain(|d| d.kind == DiagnosticKind::Warning);
339
340 Ok(diagnostics)
341 }
342
343 async fn run_clippy(&self) -> Result<String> {
345 let mut cmd = Command::new("cargo");
346 cmd.args(["clippy", "--message-format=json", "--", "-W", "clippy::all"])
347 .current_dir(&self.workspace_root)
348 .stdout(Stdio::piped())
349 .stderr(Stdio::piped())
350 .stdin(Stdio::null());
351
352 let mut child = cmd.spawn().map_err(PawanError::Io)?;
353
354 let mut stdout = String::new();
355 let mut stderr = String::new();
356
357 if let Some(mut handle) = child.stdout.take() {
358 handle.read_to_string(&mut stdout).await.ok();
359 }
360
361 if let Some(mut handle) = child.stderr.take() {
362 handle.read_to_string(&mut stderr).await.ok();
363 }
364
365 child.wait().await.map_err(PawanError::Io)?;
366
367 Ok(format!("{}\n{}", stdout, stderr))
368 }
369}
370
371pub struct TestFixer {
378 workspace_root: PathBuf,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct FailedTest {
389 pub name: String,
391 pub module: String,
393 pub failure: String,
395 pub file: Option<PathBuf>,
397 pub line: Option<usize>,
399}
400
401impl TestFixer {
402 pub fn new(workspace_root: PathBuf) -> Self {
404 Self { workspace_root }
405 }
406
407 pub async fn check(&self) -> Result<Vec<FailedTest>> {
409 let output = self.run_tests().await?;
410 Ok(self.parse_test_output(&output))
411 }
412
413 async fn run_tests(&self) -> Result<String> {
415 let mut cmd = Command::new("cargo");
416 cmd.args(["test", "--no-fail-fast", "--", "--nocapture"])
417 .current_dir(&self.workspace_root)
418 .stdout(Stdio::piped())
419 .stderr(Stdio::piped())
420 .stdin(Stdio::null());
421
422 let mut child = cmd.spawn().map_err(PawanError::Io)?;
423
424 let mut stdout = String::new();
425 let mut stderr = String::new();
426
427 if let Some(mut handle) = child.stdout.take() {
428 handle.read_to_string(&mut stdout).await.ok();
429 }
430
431 if let Some(mut handle) = child.stderr.take() {
432 handle.read_to_string(&mut stderr).await.ok();
433 }
434
435 child.wait().await.map_err(PawanError::Io)?;
436
437 Ok(format!("{}\n{}", stdout, stderr))
438 }
439
440 fn parse_test_output(&self, output: &str) -> Vec<FailedTest> {
442 let mut failures = Vec::new();
443 let mut in_failures_section = false;
444 let mut current_test: Option<String> = None;
445 let mut current_output = String::new();
446
447 for line in output.lines() {
448 if line.contains("failures:") && !line.contains("test result:") {
450 in_failures_section = true;
451 continue;
452 }
453
454 if in_failures_section && line.starts_with("test result:") {
456 if let Some(test_name) = current_test.take() {
458 failures.push(FailedTest {
459 name: test_name.clone(),
460 module: self.extract_module(&test_name),
461 failure: current_output.trim().to_string(),
462 file: None,
463 line: None,
464 });
465 }
466 break;
467 }
468
469 if line.starts_with("---- ") && line.ends_with(" stdout ----") {
471 if let Some(test_name) = current_test.take() {
473 failures.push(FailedTest {
474 name: test_name.clone(),
475 module: self.extract_module(&test_name),
476 failure: current_output.trim().to_string(),
477 file: None,
478 line: None,
479 });
480 }
481
482 let test_name = line
484 .trim_start_matches("---- ")
485 .trim_end_matches(" stdout ----")
486 .to_string();
487 current_test = Some(test_name);
488 current_output.clear();
489 } else if current_test.is_some() {
490 current_output.push_str(line);
491 current_output.push('\n');
492 }
493 }
494
495 for line in output.lines() {
497 if line.contains("FAILED") && line.starts_with("test ") {
498 let parts: Vec<&str> = line.split_whitespace().collect();
499 if parts.len() >= 2 {
500 let test_name = parts[1].trim_end_matches(" ...");
501
502 if !failures.iter().any(|f| f.name == test_name) {
504 failures.push(FailedTest {
505 name: test_name.to_string(),
506 module: self.extract_module(test_name),
507 failure: line.to_string(),
508 file: None,
509 line: None,
510 });
511 }
512 }
513 }
514 }
515
516 failures
517 }
518
519 fn extract_module(&self, test_name: &str) -> String {
521 if let Some(pos) = test_name.rfind("::") {
522 test_name[..pos].to_string()
523 } else {
524 String::new()
525 }
526 }
527}
528
529pub struct Healer {
540 #[allow(dead_code)]
541 workspace_root: PathBuf,
542 config: HealingConfig,
543 compiler_fixer: CompilerFixer,
544 clippy_fixer: ClippyFixer,
545 test_fixer: TestFixer,
546}
547
548impl Healer {
549 pub fn new(workspace_root: PathBuf, config: HealingConfig) -> Self {
551 Self {
552 compiler_fixer: CompilerFixer::new(workspace_root.clone()),
553 clippy_fixer: ClippyFixer::new(workspace_root.clone()),
554 test_fixer: TestFixer::new(workspace_root.clone()),
555 workspace_root,
556 config,
557 }
558 }
559
560 pub async fn get_diagnostics(&self) -> Result<Vec<Diagnostic>> {
568 let mut all = Vec::new();
569
570 if self.config.fix_errors {
571 all.extend(self.compiler_fixer.check().await?);
572 }
573
574 if self.config.fix_warnings {
575 all.extend(self.clippy_fixer.check().await?);
576 }
577
578 Ok(all)
579 }
580
581 pub async fn get_failed_tests(&self) -> Result<Vec<FailedTest>> {
588 if self.config.fix_tests {
589 self.test_fixer.check().await
590 } else {
591 Ok(Vec::new())
592 }
593 }
594
595 pub async fn count_issues(&self) -> Result<(usize, usize, usize)> {
602 let diagnostics = self.get_diagnostics().await?;
603 let tests = self.get_failed_tests().await?;
604
605 let errors = diagnostics
606 .iter()
607 .filter(|d| d.kind == DiagnosticKind::Error)
608 .count();
609 let warnings = diagnostics
610 .iter()
611 .filter(|d| d.kind == DiagnosticKind::Warning)
612 .count();
613 let failed_tests = tests.len();
614
615 Ok((errors, warnings, failed_tests))
616 }
617
618 pub fn format_diagnostics_for_prompt(&self, diagnostics: &[Diagnostic]) -> String {
629 let mut output = String::new();
630
631 for (i, d) in diagnostics.iter().enumerate() {
632 output.push_str(&format!("\n### Issue {}\n", i + 1));
633 output.push_str(&format!("Type: {:?}\n", d.kind));
634
635 if let Some(ref code) = d.code {
636 output.push_str(&format!("Code: {}\n", code));
637 }
638
639 output.push_str(&format!("Message: {}\n", d.message));
640
641 if let Some(ref file) = d.file {
642 output.push_str(&format!(
643 "Location: {}:{}:{}\n",
644 file.display(),
645 d.line.unwrap_or(0),
646 d.column.unwrap_or(0)
647 ));
648 }
649
650 if let Some(ref suggestion) = d.suggestion {
651 output.push_str(&format!("Suggestion: {}\n", suggestion));
652 }
653 }
654
655 output
656 }
657
658 pub fn format_tests_for_prompt(&self, tests: &[FailedTest]) -> String {
660 let mut output = String::new();
661
662 for (i, test) in tests.iter().enumerate() {
663 output.push_str(&format!("\n### Failed Test {}\n", i + 1));
664 output.push_str(&format!("Name: {}\n", test.name));
665 output.push_str(&format!("Module: {}\n", test.module));
666 output.push_str(&format!("Failure:\n```\n{}\n```\n", test.failure));
667 }
668
669 output
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676
677 #[test]
678 fn test_parse_text_diagnostic() {
679 let output = r#"error[E0425]: cannot find value `x` in this scope
680 --> src/main.rs:10:5
681 |
68210 | x
683 | ^ not found in this scope
684"#;
685
686 let fixer = CompilerFixer::new(PathBuf::from("."));
687 let diagnostics = fixer.parse_text_diagnostics(output);
688
689 assert_eq!(diagnostics.len(), 1);
690 assert_eq!(diagnostics[0].kind, DiagnosticKind::Error);
691 assert_eq!(diagnostics[0].code, Some("E0425".to_string()));
692 assert!(diagnostics[0].message.contains("cannot find value"));
693 }
694
695 #[test]
696 fn test_extract_module() {
697 let fixer = TestFixer::new(PathBuf::from("."));
698
699 assert_eq!(
700 fixer.extract_module("crate::module::tests::test_foo"),
701 "crate::module::tests"
702 );
703 assert_eq!(fixer.extract_module("test_foo"), "");
704 }
705}