1use serde::Deserialize;
6
7#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
9pub struct RustcDiagnostic {
10 pub message: String,
12
13 pub code: Option<RustcCode>,
15
16 pub level: String,
18
19 pub spans: Vec<RustcSpan>,
21
22 pub children: Vec<RustcDiagnostic>,
24
25 pub rendered: Option<String>,
27}
28
29impl RustcDiagnostic {
30 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
32 serde_json::from_str(json)
33 }
34
35 pub fn from_json_lines(json_lines: &str) -> Result<Vec<Self>, serde_json::Error> {
37 json_lines
38 .lines()
39 .filter(|line| !line.trim().is_empty())
40 .map(serde_json::from_str)
41 .collect()
42 }
43
44 pub fn primary_span(&self) -> Option<&RustcSpan> {
46 self.spans.iter().find(|s| s.is_primary)
47 }
48
49 pub fn primary_position(&self) -> Option<(String, usize, usize)> {
51 self.primary_span().map(|span| (span.file_name.clone(), span.line_start, span.column_start))
52 }
53
54 pub fn is_error(&self) -> bool {
56 self.level == "error"
57 }
58
59 pub fn is_warning(&self) -> bool {
61 self.level == "warning"
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
67pub struct RustcCode {
68 pub code: String,
70
71 #[serde(default)]
73 pub explanation: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
78pub struct RustcSpan {
79 pub file_name: String,
81
82 pub byte_start: usize,
84
85 pub byte_end: usize,
87
88 pub line_start: usize,
90
91 pub line_end: usize,
93
94 pub column_start: usize,
96
97 pub column_end: usize,
99
100 pub is_primary: bool,
102
103 pub text: Vec<RustcSpanText>,
105
106 pub label: Option<String>,
108
109 #[serde(default)]
111 pub suggested_replacement: Option<String>,
112
113 #[serde(default)]
115 pub suggestion_applicability: Option<String>,
116
117 #[serde(default)]
119 pub expansion: Option<Box<RustcExpansion>>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
124pub struct RustcSpanText {
125 pub text: String,
127
128 pub highlight_start: usize,
130
131 pub highlight_end: usize,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
137pub struct RustcExpansion {
138 pub span: RustcSpan,
140
141 pub macro_decl_name: String,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_parse_simple_error() {
151 let json = r#"{
152 "message": "cannot find value `x` in this scope",
153 "code": {
154 "code": "E0425",
155 "explanation": null
156 },
157 "level": "error",
158 "spans": [
159 {
160 "file_name": "test.rs",
161 "byte_start": 42,
162 "byte_end": 43,
163 "line_start": 3,
164 "line_end": 3,
165 "column_start": 5,
166 "column_end": 6,
167 "is_primary": true,
168 "text": [
169 {
170 "text": " x",
171 "highlight_start": 5,
172 "highlight_end": 6
173 }
174 ],
175 "label": "not found in this scope",
176 "suggested_replacement": null,
177 "suggestion_applicability": null,
178 "expansion": null
179 }
180 ],
181 "children": [],
182 "rendered": null
183 }"#;
184
185 let diagnostic = RustcDiagnostic::from_json(json).unwrap();
186 assert_eq!(diagnostic.message, "cannot find value `x` in this scope");
187 assert_eq!(diagnostic.level, "error");
188 assert!(diagnostic.is_error());
189 assert!(!diagnostic.is_warning());
190 assert_eq!(diagnostic.spans.len(), 1);
191
192 let span = &diagnostic.spans[0];
193 assert_eq!(span.file_name, "test.rs");
194 assert_eq!(span.line_start, 3);
195 assert_eq!(span.column_start, 5);
196 assert!(span.is_primary);
197 }
198
199 #[test]
200 fn test_primary_position() {
201 let json = r#"{
202 "message": "test error",
203 "code": null,
204 "level": "error",
205 "spans": [
206 {
207 "file_name": "test.rs",
208 "byte_start": 0,
209 "byte_end": 1,
210 "line_start": 1,
211 "line_end": 1,
212 "column_start": 1,
213 "column_end": 2,
214 "is_primary": true,
215 "text": [],
216 "label": null,
217 "suggested_replacement": null,
218 "suggestion_applicability": null,
219 "expansion": null
220 }
221 ],
222 "children": [],
223 "rendered": null
224 }"#;
225
226 let diagnostic = RustcDiagnostic::from_json(json).unwrap();
227 let (file, line, col) = diagnostic.primary_position().unwrap();
228 assert_eq!(file, "test.rs");
229 assert_eq!(line, 1);
230 assert_eq!(col, 1);
231 }
232
233 #[test]
234 fn test_parse_warning() {
235 let json = r#"{
236 "message": "unused variable",
237 "code": null,
238 "level": "warning",
239 "spans": [],
240 "children": [],
241 "rendered": null
242 }"#;
243
244 let diagnostic = RustcDiagnostic::from_json(json).unwrap();
245 assert!(diagnostic.is_warning());
246 assert!(!diagnostic.is_error());
247 }
248
249 #[test]
250 fn test_parse_multiple_diagnostics() {
251 let json_lines = r#"{"message": "error 1", "code": null, "level": "error", "spans": [], "children": [], "rendered": null}
252{"message": "error 2", "code": null, "level": "error", "spans": [], "children": [], "rendered": null}"#;
253
254 let diagnostics = RustcDiagnostic::from_json_lines(json_lines).unwrap();
255 assert_eq!(diagnostics.len(), 2);
256 assert_eq!(diagnostics[0].message, "error 1");
257 assert_eq!(diagnostics[1].message, "error 2");
258 }
259
260 #[test]
261 fn test_no_primary_span() {
262 let json = r#"{
263 "message": "note",
264 "code": null,
265 "level": "note",
266 "spans": [],
267 "children": [],
268 "rendered": null
269 }"#;
270
271 let diagnostic = RustcDiagnostic::from_json(json).unwrap();
272 assert!(diagnostic.primary_span().is_none());
273 assert!(diagnostic.primary_position().is_none());
274 }
275
276 #[test]
277 fn test_error_code_extraction() {
278 let json = r#"{
279 "message": "test",
280 "code": {
281 "code": "E0425",
282 "explanation": "Some explanation"
283 },
284 "level": "error",
285 "spans": [],
286 "children": [],
287 "rendered": null
288 }"#;
289
290 let diagnostic = RustcDiagnostic::from_json(json).unwrap();
291 assert!(diagnostic.code.is_some());
292 let code = diagnostic.code.unwrap();
293 assert_eq!(code.code, "E0425");
294 assert_eq!(code.explanation, Some("Some explanation".to_string()));
295 }
296}