1use std::path::PathBuf;
4
5use serde::Deserialize;
6
7#[derive(Debug, Clone)]
9pub struct CompileError {
10 pub message: String,
12
13 pub code: Option<String>,
15
16 pub level: ErrorLevel,
18
19 pub location: Option<SourceLocation>,
21
22 pub spans: Vec<ErrorSpan>,
24
25 pub rendered: Option<String>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ErrorLevel {
32 Error,
33 Warning,
34 Note,
35 Help,
36}
37
38#[derive(Debug, Clone)]
40pub struct SourceLocation {
41 pub file: PathBuf,
43
44 pub line: usize,
46
47 pub column: usize,
49}
50
51#[derive(Debug, Clone)]
53pub struct ErrorSpan {
54 pub location: SourceLocation,
56
57 pub end_location: Option<SourceLocation>,
59
60 pub label: Option<String>,
62
63 pub is_primary: bool,
65}
66
67#[derive(Debug, Deserialize)]
69pub struct RustcDiagnostic {
70 pub message: String,
71 pub code: Option<RustcCode>,
72 pub level: String,
73 pub spans: Vec<RustcSpan>,
74 pub rendered: Option<String>,
75}
76
77#[derive(Debug, Deserialize)]
78pub struct RustcCode {
79 pub code: String,
80}
81
82#[derive(Debug, Deserialize)]
83pub struct RustcSpan {
84 #[allow(dead_code)] pub file_name: String,
86 pub line_start: usize,
87 pub line_end: usize,
88 pub column_start: usize,
89 pub column_end: usize,
90 pub is_primary: bool,
91 pub label: Option<String>,
92}
93
94pub struct ErrorMapper {
96 line_map: Vec<LineMapping>,
98
99 original_file: PathBuf,
101}
102
103#[derive(Debug, Clone)]
105struct LineMapping {
106 generated_line: usize,
108
109 original_line: usize,
111
112 #[allow(dead_code)] column_offset: isize,
115}
116
117impl ErrorMapper {
118 pub fn new(original_file: PathBuf) -> Self {
120 Self {
121 line_map: Vec::new(),
122 original_file,
123 }
124 }
125
126 pub fn add_mapping(&mut self, generated_line: usize, original_line: usize) {
128 self.line_map.push(LineMapping {
129 generated_line,
130 original_line,
131 column_offset: 0,
132 });
133 }
134
135 pub fn parse_rustc_output(&self, json_output: &str) -> Vec<CompileError> {
137 let mut errors = Vec::new();
138
139 for line in json_output.lines() {
140 if line.trim().is_empty() {
141 continue;
142 }
143
144 match serde_json::from_str::<RustcDiagnostic>(line) {
146 Ok(diagnostic) => {
147 if let Some(error) = self.map_diagnostic(&diagnostic) {
148 errors.push(error);
149 }
150 }
151 Err(e) => {
152 tracing::debug!(
154 "Failed to parse rustc JSON: {} (line: {})",
155 e,
156 if line.len() > 100 { &line[..100] } else { line }
157 );
158 }
159 }
160 }
161
162 errors
163 }
164
165 fn map_diagnostic(&self, diagnostic: &RustcDiagnostic) -> Option<CompileError> {
167 let level = match diagnostic.level.as_str() {
168 "error" => ErrorLevel::Error,
169 "warning" => ErrorLevel::Warning,
170 "note" => ErrorLevel::Note,
171 "help" => ErrorLevel::Help,
172 _ => return None, };
174
175 let primary_span = diagnostic.spans.iter().find(|s| s.is_primary);
177
178 let location = primary_span.map(|span| self.map_location(span));
179
180 let spans: Vec<ErrorSpan> = diagnostic
181 .spans
182 .iter()
183 .map(|span| ErrorSpan {
184 location: self.map_location(span),
185 end_location: if span.line_start != span.line_end {
186 Some(SourceLocation {
187 file: self.original_file.clone(),
188 line: self.map_line(span.line_end),
189 column: span.column_end,
190 })
191 } else {
192 None
193 },
194 label: span.label.clone(),
195 is_primary: span.is_primary,
196 })
197 .collect();
198
199 Some(CompileError {
200 message: diagnostic.message.clone(),
201 code: diagnostic.code.as_ref().map(|c| c.code.clone()),
202 level,
203 location,
204 spans,
205 rendered: diagnostic.rendered.clone(),
206 })
207 }
208
209 fn map_location(&self, span: &RustcSpan) -> SourceLocation {
211 SourceLocation {
212 file: self.original_file.clone(),
213 line: self.map_line(span.line_start),
214 column: span.column_start,
215 }
216 }
217
218 fn map_line(&self, generated_line: usize) -> usize {
220 for mapping in &self.line_map {
222 if mapping.generated_line == generated_line {
223 return mapping.original_line;
224 }
225 }
226
227 if let Some(mapping) = self
229 .line_map
230 .iter()
231 .min_by_key(|m| (m.generated_line as isize - generated_line as isize).unsigned_abs())
232 {
233 let offset = generated_line as isize - mapping.generated_line as isize;
234 return (mapping.original_line as isize + offset).max(1) as usize;
235 }
236
237 generated_line
239 }
240}
241
242impl CompileError {
243 pub fn simple(message: impl Into<String>) -> Vec<Self> {
245 vec![Self {
246 message: message.into(),
247 code: None,
248 level: ErrorLevel::Error,
249 location: None,
250 spans: Vec::new(),
251 rendered: None,
252 }]
253 }
254
255 pub fn simple_rendered(message: impl Into<String>) -> Vec<Self> {
257 let msg = message.into();
258 vec![Self {
259 message: msg.clone(),
260 code: None,
261 level: ErrorLevel::Error,
262 location: None,
263 spans: Vec::new(),
264 rendered: Some(msg),
265 }]
266 }
267
268 pub fn format_terminal(&self) -> String {
270 let mut output = String::new();
271
272 let level_str = match self.level {
274 ErrorLevel::Error => "\x1b[1;31merror\x1b[0m",
275 ErrorLevel::Warning => "\x1b[1;33mwarning\x1b[0m",
276 ErrorLevel::Note => "\x1b[1;36mnote\x1b[0m",
277 ErrorLevel::Help => "\x1b[1;32mhelp\x1b[0m",
278 };
279
280 if let Some(code) = &self.code {
281 output.push_str(&format!("{level_str}[{code}]: {}\n", self.message));
282 } else {
283 output.push_str(&format!("{level_str}: {}\n", self.message));
284 }
285
286 if let Some(loc) = &self.location {
288 output.push_str(&format!(
289 " \x1b[1;34m-->\x1b[0m {}:{}:{}\n",
290 loc.file.display(),
291 loc.line,
292 loc.column
293 ));
294 }
295
296 output
297 }
298
299 pub fn to_json(&self) -> serde_json::Value {
301 serde_json::json!({
302 "message": self.message,
303 "code": self.code,
304 "level": format!("{:?}", self.level).to_lowercase(),
305 "location": self.location.as_ref().map(|loc| {
306 serde_json::json!({
307 "file": loc.file.display().to_string(),
308 "line": loc.line,
309 "column": loc.column,
310 })
311 }),
312 })
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_parse_rustc_json() {
322 let json = r#"{"message":"expected type, found `42`","code":{"code":"E0573"},"level":"error","spans":[{"file_name":"test.rs","line_start":5,"line_end":5,"column_start":10,"column_end":12,"is_primary":true,"label":"expected type"}],"rendered":"error[E0573]: expected type, found `42`"}"#;
323
324 let mapper = ErrorMapper::new(PathBuf::from("test.rs"));
325 let errors = mapper.parse_rustc_output(json);
326
327 assert_eq!(errors.len(), 1);
328 assert_eq!(errors[0].code, Some("E0573".to_string()));
329 assert_eq!(errors[0].level, ErrorLevel::Error);
330 assert!(errors[0].message.contains("expected type"));
331 }
332
333 #[test]
334 fn test_line_mapping() {
335 let mut mapper = ErrorMapper::new(PathBuf::from("original.rs"));
336 mapper.add_mapping(10, 5); mapper.add_mapping(20, 15); assert_eq!(mapper.map_line(10), 5);
340 assert_eq!(mapper.map_line(20), 15);
341 assert_eq!(mapper.map_line(15), 10);
343 }
344
345 #[test]
346 fn test_error_format() {
347 let error = CompileError {
348 message: "test error".to_string(),
349 code: Some("E0001".to_string()),
350 level: ErrorLevel::Error,
351 location: Some(SourceLocation {
352 file: PathBuf::from("test.rs"),
353 line: 10,
354 column: 5,
355 }),
356 spans: Vec::new(),
357 rendered: None,
358 };
359
360 let formatted = error.format_terminal();
361 assert!(formatted.contains("error"));
362 assert!(formatted.contains("E0001"));
363 assert!(formatted.contains("test.rs:10:5"));
364 }
365}