cargo_context_core/collect/
errors.rs1use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::{Error, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum DiagLevel {
17 Error,
18 Warning,
19 Note,
20 Help,
21 Ice,
22 Other,
23}
24
25impl DiagLevel {
26 fn from_cargo(l: &cargo_metadata::diagnostic::DiagnosticLevel) -> Self {
27 use cargo_metadata::diagnostic::DiagnosticLevel as L;
28 match l {
29 L::Error => DiagLevel::Error,
30 L::Warning => DiagLevel::Warning,
31 L::Note => DiagLevel::Note,
32 L::Help => DiagLevel::Help,
33 L::Ice => DiagLevel::Ice,
34 L::FailureNote => DiagLevel::Note,
35 _ => DiagLevel::Other,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct DiagSpan {
42 pub file: PathBuf,
43 pub line_start: usize,
44 pub line_end: usize,
45 pub col_start: usize,
46 pub col_end: usize,
47 pub is_primary: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Diagnostic {
52 pub level: DiagLevel,
53 pub message: String,
54 pub code: Option<String>,
55 pub spans: Vec<DiagSpan>,
56 pub rendered: String,
57}
58
59impl Diagnostic {
60 pub fn primary_file(&self) -> Option<&Path> {
62 self.spans
63 .iter()
64 .find(|s| s.is_primary)
65 .map(|s| s.file.as_path())
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Diagnostics {
71 pub success: bool,
72 pub diagnostics: Vec<Diagnostic>,
73}
74
75impl Diagnostics {
76 pub fn is_empty(&self) -> bool {
77 self.diagnostics.is_empty()
78 }
79
80 pub fn referenced_files(&self) -> Vec<PathBuf> {
82 let mut paths: Vec<PathBuf> = self
83 .diagnostics
84 .iter()
85 .flat_map(|d| d.spans.iter().filter(|s| s.is_primary))
86 .map(|s| s.file.clone())
87 .collect();
88 paths.sort();
89 paths.dedup();
90 paths
91 }
92
93 pub fn has_errors(&self) -> bool {
94 self.diagnostics
95 .iter()
96 .any(|d| d.level == DiagLevel::Error || d.level == DiagLevel::Ice)
97 }
98}
99
100pub fn last_error(root: &Path) -> Result<Diagnostics> {
106 let output = Command::new("cargo")
107 .current_dir(root)
108 .args([
109 "check",
110 "--workspace",
111 "--all-targets",
112 "--message-format=json",
113 "--color=never",
114 ])
115 .output()
116 .map_err(|e| Error::Tool(format!("failed to spawn cargo: {e}")))?;
117
118 let stream = String::from_utf8_lossy(&output.stdout);
119 let diagnostics = parse_message_stream(&stream);
120
121 Ok(Diagnostics {
122 success: output.status.success(),
123 diagnostics,
124 })
125}
126
127pub fn parse_message_stream(stream: &str) -> Vec<Diagnostic> {
130 use cargo_metadata::Message;
131
132 let mut out = Vec::new();
133 for line in stream.lines() {
134 let trimmed = line.trim();
135 if trimmed.is_empty() || !trimmed.starts_with('{') {
136 continue;
137 }
138 let msg: Message = match serde_json::from_str(trimmed) {
139 Ok(m) => m,
140 Err(_) => continue,
141 };
142 if let Message::CompilerMessage(cm) = msg {
143 out.push(convert(cm.message));
144 }
145 }
146 out
147}
148
149fn convert(d: cargo_metadata::diagnostic::Diagnostic) -> Diagnostic {
150 let spans = d
151 .spans
152 .iter()
153 .map(|s| DiagSpan {
154 file: PathBuf::from(&s.file_name),
155 line_start: s.line_start,
156 line_end: s.line_end,
157 col_start: s.column_start,
158 col_end: s.column_end,
159 is_primary: s.is_primary,
160 })
161 .collect();
162 Diagnostic {
163 level: DiagLevel::from_cargo(&d.level),
164 message: d.message,
165 code: d.code.map(|c| c.code),
166 spans,
167 rendered: d.rendered.unwrap_or_default(),
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 const SAMPLE: &str = r#"{"reason":"compiler-message","package_id":"foo 0.1.0","manifest_path":"/tmp/foo/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"foo","src_path":"/tmp/foo/src/lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"message":{"message":"mismatched types","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"src/lib.rs","byte_start":10,"byte_end":20,"line_start":3,"line_end":3,"column_start":5,"column_end":12,"is_primary":true,"text":[],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error[E0308]: mismatched types\n --> src/lib.rs:3:5\n"}}
176{"reason":"build-finished","success":false}
177"#;
178
179 #[test]
180 fn parses_compiler_message_stream() {
181 let diags = parse_message_stream(SAMPLE);
182 assert_eq!(diags.len(), 1);
183 assert_eq!(diags[0].level, DiagLevel::Error);
184 assert_eq!(diags[0].code.as_deref(), Some("E0308"));
185 assert_eq!(diags[0].spans.len(), 1);
186 assert!(diags[0].spans[0].is_primary);
187 }
188
189 #[test]
190 fn referenced_files_deduplicates() {
191 let diags = parse_message_stream(SAMPLE);
192 let d = Diagnostics {
193 success: false,
194 diagnostics: diags,
195 };
196 let files = d.referenced_files();
197 assert_eq!(files, vec![PathBuf::from("src/lib.rs")]);
198 }
199
200 #[test]
201 fn ignores_non_message_lines() {
202 let noisy = "garbage\n{\"reason\":\"build-finished\",\"success\":true}\n";
203 assert!(parse_message_stream(noisy).is_empty());
204 }
205}