1use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RustDiagnostic {
24 pub level: String,
26 pub message: String,
28 #[serde(default)]
30 pub spans: Vec<DiagnosticSpan>,
31 pub code: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DiagnosticSpan {
38 pub file_name: String,
40 pub line_start: u32,
42 pub line_end: u32,
44 pub column_start: u32,
46 pub column_end: u32,
48 #[serde(default)]
50 pub text: Vec<SpanText>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct SpanText {
56 pub text: String,
58 pub highlight_start: u32,
60 pub highlight_end: u32,
62}
63
64#[derive(Debug, Deserialize)]
69struct RawDiagnostic {
70 level: String,
71 message: String,
72 #[serde(default)]
73 spans: Vec<RawSpan>,
74 code: Option<RawCode>,
75}
76
77#[derive(Debug, Deserialize)]
78struct RawCode {
79 code: String,
80}
81
82#[derive(Debug, Deserialize)]
83struct RawSpan {
84 file_name: String,
85 line_start: u32,
86 line_end: u32,
87 column_start: u32,
88 column_end: u32,
89 #[serde(default)]
90 text: Vec<RawSpanText>,
91}
92
93#[derive(Debug, Deserialize)]
94struct RawSpanText {
95 text: String,
96 highlight_start: u32,
97 highlight_end: u32,
98}
99
100pub fn parse_diagnostics(stderr: &str) -> Vec<RustDiagnostic> {
115 stderr
116 .lines()
117 .filter_map(|line| {
118 let line = line.trim();
119 if line.is_empty() {
120 return None;
121 }
122 let raw: RawDiagnostic = serde_json::from_str(line).ok()?;
123 Some(RustDiagnostic {
124 level: raw.level,
125 message: raw.message,
126 spans: raw
127 .spans
128 .into_iter()
129 .map(|s| DiagnosticSpan {
130 file_name: s.file_name,
131 line_start: s.line_start,
132 line_end: s.line_end,
133 column_start: s.column_start,
134 column_end: s.column_end,
135 text: s
136 .text
137 .into_iter()
138 .map(|t| SpanText {
139 text: t.text,
140 highlight_start: t.highlight_start,
141 highlight_end: t.highlight_end,
142 })
143 .collect(),
144 })
145 .collect(),
146 code: raw.code.map(|c| c.code),
147 })
148 })
149 .collect()
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn parse_empty_stderr() {
158 let diagnostics = parse_diagnostics("");
159 assert!(diagnostics.is_empty());
160 }
161
162 #[test]
163 fn parse_non_json_lines_skipped() {
164 let stderr = "some random text\nnot json at all\n";
165 let diagnostics = parse_diagnostics(stderr);
166 assert!(diagnostics.is_empty());
167 }
168
169 #[test]
170 fn parse_single_error_diagnostic() {
171 let stderr = r#"{"message":"expected `;`","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"main.rs","byte_start":10,"byte_end":11,"line_start":1,"line_end":1,"column_start":11,"column_end":12,"is_primary":true,"text":[{"text":"let x = 1","highlight_start":11,"highlight_end":12}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error: expected `;`"}"#;
172 let diagnostics = parse_diagnostics(stderr);
173 assert_eq!(diagnostics.len(), 1);
174 assert_eq!(diagnostics[0].level, "error");
175 assert_eq!(diagnostics[0].message, "expected `;`");
176 assert_eq!(diagnostics[0].code.as_deref(), Some("E0308"));
177 assert_eq!(diagnostics[0].spans.len(), 1);
178 assert_eq!(diagnostics[0].spans[0].file_name, "main.rs");
179 assert_eq!(diagnostics[0].spans[0].line_start, 1);
180 assert_eq!(diagnostics[0].spans[0].column_start, 11);
181 assert_eq!(diagnostics[0].spans[0].text.len(), 1);
182 assert_eq!(diagnostics[0].spans[0].text[0].text, "let x = 1");
183 }
184
185 #[test]
186 fn parse_warning_without_code() {
187 let stderr = r#"{"message":"unused variable: `x`","code":null,"level":"warning","spans":[],"children":[],"rendered":"warning: unused variable"}"#;
188 let diagnostics = parse_diagnostics(stderr);
189 assert_eq!(diagnostics.len(), 1);
190 assert_eq!(diagnostics[0].level, "warning");
191 assert!(diagnostics[0].code.is_none());
192 assert!(diagnostics[0].spans.is_empty());
193 }
194
195 #[test]
196 fn parse_mixed_json_and_text() {
197 let stderr = format!(
198 "{}\nerror[E0308]: mismatched types\n{}",
199 r#"{"message":"type mismatch","code":{"code":"E0308","explanation":null},"level":"error","spans":[],"children":[],"rendered":"error"}"#,
200 r#"{"message":"help: consider","code":null,"level":"help","spans":[],"children":[],"rendered":"help"}"#,
201 );
202 let diagnostics = parse_diagnostics(&stderr);
203 assert_eq!(diagnostics.len(), 2);
204 assert_eq!(diagnostics[0].level, "error");
205 assert_eq!(diagnostics[1].level, "help");
206 }
207
208 #[test]
209 fn parse_diagnostic_with_multiple_spans() {
210 let stderr = r#"{"message":"mismatched types","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"main.rs","byte_start":10,"byte_end":11,"line_start":1,"line_end":1,"column_start":11,"column_end":12,"is_primary":true,"text":[{"text":"let x: i32 = \"hello\"","highlight_start":15,"highlight_end":22}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"main.rs","byte_start":20,"byte_end":25,"line_start":2,"line_end":2,"column_start":5,"column_end":10,"is_primary":false,"text":[{"text":" x + 1","highlight_start":5,"highlight_end":10}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error"}"#;
211 let diagnostics = parse_diagnostics(stderr);
212 assert_eq!(diagnostics.len(), 1);
213 assert_eq!(diagnostics[0].spans.len(), 2);
214 assert_eq!(diagnostics[0].spans[0].line_start, 1);
215 assert_eq!(diagnostics[0].spans[1].line_start, 2);
216 }
217}