sea_core/error/
diagnostics.rs

1/// Diagnostic formatters for ValidationError
2///
3/// Provides multiple output formats for error diagnostics:
4/// - JSON: Machine-readable format for CI/CD tools
5/// - Human: Color-coded format with source snippets for developers
6/// - LSP: Language Server Protocol compatible format for IDEs
7use crate::validation_error::{SourceRange, ValidationError};
8use serde::{Deserialize, Serialize};
9
10/// Trait for formatting validation errors
11pub trait DiagnosticFormatter {
12    /// Format a validation error with optional source code
13    fn format(&self, error: &ValidationError, source: Option<&str>) -> String;
14}
15
16/// JSON diagnostic format for machine parsing (CI/CD tools)
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct JsonDiagnostic {
19    pub code: String,
20    pub severity: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub range: Option<JsonRange>,
23    pub message: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub hint: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct JsonRange {
30    pub start: JsonPosition,
31    pub end: JsonPosition,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonPosition {
36    pub line: usize,
37    pub column: usize,
38}
39
40impl From<SourceRange> for JsonRange {
41    fn from(range: SourceRange) -> Self {
42        JsonRange {
43            start: JsonPosition {
44                line: range.start.line,
45                column: range.start.column,
46            },
47            end: JsonPosition {
48                line: range.end.line,
49                column: range.end.column,
50            },
51        }
52    }
53}
54
55impl JsonDiagnostic {
56    pub fn from_validation_error(error: &ValidationError) -> Self {
57        let code = error.error_code();
58        let severity = Self::determine_severity(error);
59        let range = error.range().map(JsonRange::from);
60        let message = error.to_string();
61        let hint = Self::extract_hint(error);
62
63        JsonDiagnostic {
64            code: code.as_str().to_string(),
65            severity,
66            range,
67            message,
68            hint,
69        }
70    }
71
72    fn determine_severity(error: &ValidationError) -> String {
73        match error {
74            ValidationError::SyntaxError { .. }
75            | ValidationError::TypeError { .. }
76            | ValidationError::UnitError { .. }
77            | ValidationError::UndefinedReference { .. }
78            | ValidationError::DuplicateDeclaration { .. } => "error".to_string(),
79            ValidationError::DeterminismError { .. } => "warning".to_string(),
80            ValidationError::ScopeError { .. } | ValidationError::InvalidExpression { .. } => {
81                "error".to_string()
82            }
83        }
84    }
85
86    fn extract_hint(error: &ValidationError) -> Option<String> {
87        match error {
88            ValidationError::TypeError { suggestion, .. }
89            | ValidationError::UnitError { suggestion, .. }
90            | ValidationError::ScopeError { suggestion, .. }
91            | ValidationError::UndefinedReference { suggestion, .. }
92            | ValidationError::InvalidExpression { suggestion, .. } => suggestion.clone(),
93            ValidationError::DeterminismError { hint, .. } => Some(hint.clone()),
94            _ => None,
95        }
96    }
97}
98
99/// JSON formatter implementation
100pub struct JsonFormatter;
101
102impl DiagnosticFormatter for JsonFormatter {
103    fn format(&self, error: &ValidationError, _source: Option<&str>) -> String {
104        let diagnostic = JsonDiagnostic::from_validation_error(error);
105        serde_json::to_string_pretty(&diagnostic).unwrap_or_else(|_| {
106            // Fallback: create a minimal JSON object manually but safely
107            let fallback = serde_json::json!({
108                "code": error.error_code().as_str(),
109                "severity": "error",
110                "message": error.to_string()
111            });
112            serde_json::to_string(&fallback).unwrap_or_else(|_| {
113                r#"{"code": "UNKNOWN", "severity": "error", "message": "Failed to serialize error"}"#.to_string()
114            })
115        })
116    }
117}
118
119/// Format multiple errors as a JSON array
120pub fn format_errors_json(errors: &[ValidationError]) -> String {
121    let diagnostics: Vec<JsonDiagnostic> = errors
122        .iter()
123        .map(JsonDiagnostic::from_validation_error)
124        .collect();
125
126    serde_json::to_string_pretty(&diagnostics).unwrap_or_else(|_| "[]".to_string())
127}
128
129/// Human-readable diagnostic formatter with color support
130pub struct HumanFormatter {
131    pub use_color: bool,
132    pub show_source: bool,
133}
134
135impl Default for HumanFormatter {
136    fn default() -> Self {
137        Self {
138            use_color: true,
139            show_source: true,
140        }
141    }
142}
143
144impl HumanFormatter {
145    pub fn new(use_color: bool, show_source: bool) -> Self {
146        Self {
147            use_color,
148            show_source,
149        }
150    }
151
152    fn colorize(&self, text: &str, color: &str) -> String {
153        if !self.use_color {
154            return text.to_string();
155        }
156
157        let color_code = match color {
158            "red" => "\x1b[31m",
159            "yellow" => "\x1b[33m",
160            "blue" => "\x1b[34m",
161            "cyan" => "\x1b[36m",
162            "bold" => "\x1b[1m",
163            _ => "",
164        };
165
166        format!("{}{}\x1b[0m", color_code, text)
167    }
168
169    fn format_source_snippet(&self, source: &str, range: SourceRange) -> String {
170        let lines: Vec<&str> = source.lines().collect();
171        let start_line = range.start.line.saturating_sub(1); // Convert to 0-indexed
172        let end_line = range.end.line.saturating_sub(1);
173
174        if start_line >= lines.len() {
175            return String::new();
176        }
177
178        let mut output = String::new();
179        let line_num_width = (end_line + 1).to_string().len();
180
181        // Show one line before if available
182        if start_line > 0 {
183            output.push_str(&format!(
184                "{:>width$} | {}\n",
185                start_line,
186                lines[start_line - 1],
187                width = line_num_width
188            ));
189        }
190
191        let last_line = end_line.min(lines.len().saturating_sub(1));
192        if last_line < start_line {
193            return output;
194        }
195
196        for (line_idx, line) in lines
197            .iter()
198            .enumerate()
199            .skip(start_line)
200            .take(last_line.saturating_sub(start_line) + 1)
201        {
202            let line_num = line_idx + 1;
203            output.push_str(&format!(
204                "{} | {}\n",
205                self.colorize(
206                    &format!("{:>width$}", line_num, width = line_num_width),
207                    "blue"
208                ),
209                line
210            ));
211
212            // Add caret indicators for the error range
213            if line_idx == start_line {
214                let padding = " ".repeat(line_num_width + 3);
215                let start_col = range.start.column.saturating_sub(1);
216                let end_col = if line_idx == end_line {
217                    range.end.column.saturating_sub(1)
218                } else {
219                    line.len()
220                };
221
222                let caret_padding = " ".repeat(start_col);
223                let carets = "^".repeat(end_col.saturating_sub(start_col).max(1));
224                output.push_str(&format!(
225                    "{}{}{}\n",
226                    padding,
227                    caret_padding,
228                    self.colorize(&carets, "red")
229                ));
230            }
231        }
232
233        // Show one line after if available
234        if end_line + 1 < lines.len() {
235            output.push_str(&format!(
236                "{:>width$} | {}\n",
237                end_line + 2,
238                lines[end_line + 1],
239                width = line_num_width
240            ));
241        }
242
243        output
244    }
245}
246
247impl DiagnosticFormatter for HumanFormatter {
248    fn format(&self, error: &ValidationError, source: Option<&str>) -> String {
249        let code = error.error_code();
250        let severity = match error {
251            ValidationError::DeterminismError { .. } => "warning",
252            _ => "error",
253        };
254
255        let severity_colored = match severity {
256            "error" => self.colorize("error", "red"),
257            "warning" => self.colorize("warning", "yellow"),
258            _ => severity.to_string(),
259        };
260
261        let mut output = format!(
262            "{}[{}]: {}\n",
263            severity_colored,
264            self.colorize(code.as_str(), "bold"),
265            error
266        );
267
268        // Add source snippet if available
269        if self.show_source {
270            if let (Some(src), Some(range)) = (source, error.range()) {
271                output.push_str(&format!("  {} {}\n", self.colorize("-->", "blue"), range));
272                output.push_str(&self.format_source_snippet(src, range));
273            } else if let Some(location) = error.location_string() {
274                output.push_str(&format!(
275                    "  {} {}\n",
276                    self.colorize("-->", "blue"),
277                    location
278                ));
279            }
280        }
281
282        // Add hint if available
283        if let Some(hint) = JsonDiagnostic::extract_hint(error) {
284            output.push_str(&format!("  {} {}\n", self.colorize("hint:", "cyan"), hint));
285        }
286
287        output
288    }
289}
290
291/// LSP (Language Server Protocol) compatible formatter
292pub struct LspFormatter;
293
294impl DiagnosticFormatter for LspFormatter {
295    fn format(&self, error: &ValidationError, _source: Option<&str>) -> String {
296        let code = error.error_code();
297        let severity = match error {
298            ValidationError::DeterminismError { .. } => 2, // Warning
299            _ => 1,                                        // Error
300        };
301
302        let range = error.range();
303        let message = error.to_string();
304        let hint = JsonDiagnostic::extract_hint(error);
305
306        let full_message = if let Some(h) = hint {
307            format!("{}\n\n{}", message, h)
308        } else {
309            message
310        };
311
312        let range_value = if let Some(r) = range {
313            serde_json::json!({
314                "start": {
315                    "line": r.start.line.saturating_sub(1),
316                    "character": r.start.column.saturating_sub(1)
317                },
318                "end": {
319                    "line": r.end.line.saturating_sub(1),
320                    "character": r.end.column.saturating_sub(1)
321                }
322            })
323        } else {
324            serde_json::json!({
325                "start": { "line": 0, "character": 0 },
326                "end": { "line": 0, "character": 0 }
327            })
328        };
329
330        let diagnostic = serde_json::json!({
331            "range": range_value,
332            "severity": severity,
333            "code": code.as_str(),
334            "source": "sea-dsl",
335            "message": full_message
336        });
337
338        serde_json::to_string(&diagnostic).unwrap_or_else(|_| {
339            r#"{"severity": 1, "message": "Failed to serialize diagnostic"}"#.to_string()
340        })
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_json_formatter() {
350        let error = ValidationError::syntax_error("unexpected token", 10, 5);
351        let formatter = JsonFormatter;
352        let output = formatter.format(&error, None);
353
354        assert!(output.contains("E005"));
355        assert!(output.contains("error"));
356        assert!(output.contains("unexpected token"));
357    }
358
359    #[test]
360    fn test_json_formatter_with_range() {
361        let error = ValidationError::syntax_error_with_range("test", 10, 5, 10, 15);
362        let formatter = JsonFormatter;
363        let output = formatter.format(&error, None);
364
365        assert!(output.contains(r#""line": 10"#));
366        assert!(output.contains(r#""column": 5"#));
367    }
368
369    #[test]
370    fn test_json_formatter_with_hint() {
371        let candidates = vec!["Warehouse".to_string()];
372        let error =
373            ValidationError::undefined_entity_with_candidates("Warehous", "line 10", &candidates);
374        let formatter = JsonFormatter;
375        let output = formatter.format(&error, None);
376
377        assert!(output.contains("E001"));
378        assert!(output.contains("Warehouse"));
379    }
380
381    #[test]
382    fn test_human_formatter_no_color() {
383        let error = ValidationError::syntax_error("test error", 1, 1);
384        let formatter = HumanFormatter::new(false, false);
385        let output = formatter.format(&error, None);
386
387        assert!(output.contains("error[E005]"));
388        assert!(output.contains("test error"));
389        assert!(!output.contains("\x1b[")); // No ANSI codes
390    }
391
392    #[test]
393    fn test_human_formatter_with_color() {
394        let error = ValidationError::syntax_error("test error", 1, 1);
395        let formatter = HumanFormatter::new(true, false);
396        let output = formatter.format(&error, None);
397
398        assert!(output.contains("\x1b[")); // Has ANSI codes
399    }
400
401    #[test]
402    fn test_lsp_formatter() {
403        let error = ValidationError::syntax_error_with_range("test", 10, 5, 10, 15);
404        let formatter = LspFormatter;
405        let output = formatter.format(&error, None);
406
407        let json: serde_json::Value = serde_json::from_str(&output).expect("should be valid json");
408        assert_eq!(json["severity"], 1);
409        assert_eq!(json["code"], "E005");
410        assert_eq!(json["source"], "sea-dsl");
411        // LSP uses 0-indexed lines
412        assert_eq!(json["range"]["start"]["line"], 9);
413    }
414
415    #[test]
416    fn test_format_multiple_errors() {
417        let errors = vec![
418            ValidationError::syntax_error("error 1", 1, 1),
419            ValidationError::undefined_entity("Test", "line 5"),
420        ];
421
422        let output = format_errors_json(&errors);
423        assert!(output.contains("E005"));
424        assert!(output.contains("E001"));
425    }
426}