1use crate::validation_error::{SourceRange, ValidationError};
8use serde::{Deserialize, Serialize};
9
10pub trait DiagnosticFormatter {
12 fn format(&self, error: &ValidationError, source: Option<&str>) -> String;
14}
15
16#[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
99pub 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 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
119pub 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
129pub 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); 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 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 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 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 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 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
291pub 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, _ => 1, };
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[")); }
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[")); }
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 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}