venus_core/compile/
errors.rs

1//! Error handling and mapping for the compilation pipeline.
2
3use std::path::PathBuf;
4
5use serde::Deserialize;
6
7/// A compilation error with source location information.
8#[derive(Debug, Clone)]
9pub struct CompileError {
10    /// Error message
11    pub message: String,
12
13    /// Error code (e.g., "E0308")
14    pub code: Option<String>,
15
16    /// Severity level
17    pub level: ErrorLevel,
18
19    /// Primary source location
20    pub location: Option<SourceLocation>,
21
22    /// Additional spans (e.g., "help: consider...")
23    pub spans: Vec<ErrorSpan>,
24
25    /// Rendered error message (for display)
26    pub rendered: Option<String>,
27}
28
29/// Severity level of an error.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ErrorLevel {
32    Error,
33    Warning,
34    Note,
35    Help,
36}
37
38/// A location in source code.
39#[derive(Debug, Clone)]
40pub struct SourceLocation {
41    /// Source file path
42    pub file: PathBuf,
43
44    /// Line number (1-indexed)
45    pub line: usize,
46
47    /// Column number (1-indexed)
48    pub column: usize,
49}
50
51/// An error span with label.
52#[derive(Debug, Clone)]
53pub struct ErrorSpan {
54    /// Location of this span
55    pub location: SourceLocation,
56
57    /// End location (for multi-line spans)
58    pub end_location: Option<SourceLocation>,
59
60    /// Label for this span
61    pub label: Option<String>,
62
63    /// Whether this is the primary span
64    pub is_primary: bool,
65}
66
67/// Rustc JSON diagnostic format.
68#[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)] // Used for multi-file compilation debugging
85    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
94/// Maps rustc errors to original source locations.
95pub struct ErrorMapper {
96    /// Mapping from generated line to original line
97    line_map: Vec<LineMapping>,
98
99    /// Original source file path
100    original_file: PathBuf,
101}
102
103/// Mapping from generated code line to original source.
104#[derive(Debug, Clone)]
105struct LineMapping {
106    /// Line number in generated code
107    generated_line: usize,
108
109    /// Line number in original source
110    original_line: usize,
111
112    /// Column offset (if any)
113    #[allow(dead_code)] // Reserved for future column-level error mapping
114    column_offset: isize,
115}
116
117impl ErrorMapper {
118    /// Create a new error mapper for a source file.
119    pub fn new(original_file: PathBuf) -> Self {
120        Self {
121            line_map: Vec::new(),
122            original_file,
123        }
124    }
125
126    /// Add a line mapping from generated code to original source.
127    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    /// Parse rustc JSON output and map errors to original locations.
136    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            // Try to parse as JSON diagnostic
145            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                    // Log parse failures to aid debugging malformed rustc output
153                    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    /// Map a rustc diagnostic to a CompileError with corrected locations.
166    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, // Skip unknown levels
173        };
174
175        // Find primary span
176        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    /// Map a rustc span to a source location.
210    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    /// Map a generated line number to original line number.
219    fn map_line(&self, generated_line: usize) -> usize {
220        // Find the mapping entry for this line
221        for mapping in &self.line_map {
222            if mapping.generated_line == generated_line {
223                return mapping.original_line;
224            }
225        }
226
227        // If no mapping found, use a heuristic based on nearest mapping
228        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        // No mappings at all - return as-is
238        generated_line
239    }
240}
241
242impl CompileError {
243    /// Create a simple error with just a message.
244    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    /// Create a simple error with a pre-rendered message (for raw rustc output).
256    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    /// Format the error for terminal display.
269    pub fn format_terminal(&self) -> String {
270        let mut output = String::new();
271
272        // Level and message
273        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        // Location
287        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    /// Format the error for JSON output.
300    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); // Generated line 10 = original line 5
337        mapper.add_mapping(20, 15); // Generated line 20 = original line 15
338
339        assert_eq!(mapper.map_line(10), 5);
340        assert_eq!(mapper.map_line(20), 15);
341        // Interpolate for lines in between
342        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}