oxur_comp/
rustc_diagnostic.rs

1//! rustc diagnostic parser
2//!
3//! Parses JSON diagnostic output from rustc to extract error positions.
4
5use serde::Deserialize;
6
7/// A diagnostic message from rustc
8#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
9pub struct RustcDiagnostic {
10    /// The main error message
11    pub message: String,
12
13    /// Optional error code (e.g., E0425)
14    pub code: Option<RustcCode>,
15
16    /// Severity level: "error", "warning", "note", "help"
17    pub level: String,
18
19    /// Source code spans where the error occurred
20    pub spans: Vec<RustcSpan>,
21
22    /// Child diagnostics (notes, suggestions)
23    pub children: Vec<RustcDiagnostic>,
24
25    /// Rendered text output (optional)
26    pub rendered: Option<String>,
27}
28
29impl RustcDiagnostic {
30    /// Parse a rustc diagnostic from JSON string
31    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
32        serde_json::from_str(json)
33    }
34
35    /// Parse multiple diagnostics from JSON lines
36    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    /// Get the primary span (the main location of the error)
45    pub fn primary_span(&self) -> Option<&RustcSpan> {
46        self.spans.iter().find(|s| s.is_primary)
47    }
48
49    /// Get the primary position as (file, line, column)
50    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    /// Check if this is an error (vs warning or note)
55    pub fn is_error(&self) -> bool {
56        self.level == "error"
57    }
58
59    /// Check if this is a warning
60    pub fn is_warning(&self) -> bool {
61        self.level == "warning"
62    }
63}
64
65/// Error code from rustc
66#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
67pub struct RustcCode {
68    /// Error code (e.g., "E0425")
69    pub code: String,
70
71    /// Long explanation text (optional)
72    #[serde(default)]
73    pub explanation: Option<String>,
74}
75
76/// A source code span in a rustc diagnostic
77#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
78pub struct RustcSpan {
79    /// Source file path
80    pub file_name: String,
81
82    /// Byte offset start (0-indexed)
83    pub byte_start: usize,
84
85    /// Byte offset end (0-indexed)
86    pub byte_end: usize,
87
88    /// Line number start (1-indexed)
89    pub line_start: usize,
90
91    /// Line number end (1-indexed)
92    pub line_end: usize,
93
94    /// Column number start (1-indexed)
95    pub column_start: usize,
96
97    /// Column number end (1-indexed)
98    pub column_end: usize,
99
100    /// Whether this is the primary location
101    pub is_primary: bool,
102
103    /// Text snippets
104    pub text: Vec<RustcSpanText>,
105
106    /// Optional label text
107    pub label: Option<String>,
108
109    /// Optional suggested replacement
110    #[serde(default)]
111    pub suggested_replacement: Option<String>,
112
113    /// Applicability of suggestion
114    #[serde(default)]
115    pub suggestion_applicability: Option<String>,
116
117    /// Macro expansion context
118    #[serde(default)]
119    pub expansion: Option<Box<RustcExpansion>>,
120}
121
122/// Text snippet from a span
123#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
124pub struct RustcSpanText {
125    /// The source text
126    pub text: String,
127
128    /// Start of highlight in text (1-indexed)
129    pub highlight_start: usize,
130
131    /// End of highlight in text (1-indexed)
132    pub highlight_end: usize,
133}
134
135/// Macro expansion context (simplified)
136#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
137pub struct RustcExpansion {
138    /// Span where the macro was expanded
139    pub span: RustcSpan,
140
141    /// Name of the macro
142    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}