1use colored::Colorize;
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum Severity {
12 Note,
14 Warning,
16 Error,
18 Fatal,
20}
21
22impl Severity {
23 pub fn tag(&self) -> &'static str {
25 match self {
26 Self::Note => "note",
27 Self::Warning => "warning",
28 Self::Error => "error",
29 Self::Fatal => "fatal",
30 }
31 }
32}
33
34impl fmt::Display for Severity {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "{}", self.tag())
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ErrorCategory {
43 Parse,
45 Type,
47 Semantic,
49 Io,
51 Transpile,
53 Internal,
55}
56
57impl fmt::Display for ErrorCategory {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::Parse => write!(f, "parse"),
61 Self::Type => write!(f, "type"),
62 Self::Semantic => write!(f, "semantic"),
63 Self::Io => write!(f, "io"),
64 Self::Transpile => write!(f, "transpile"),
65 Self::Internal => write!(f, "internal"),
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
75pub struct Diagnostic {
76 pub severity: Severity,
78 pub message: String,
80 pub file: Option<String>,
82 pub line: Option<u32>,
84 pub column: Option<u32>,
86 pub category: Option<String>,
88 pub fix_its: Vec<String>,
90 pub snippet: Option<String>,
92 pub note: Option<String>,
94 pub help: Option<String>,
96}
97
98impl Diagnostic {
99 pub fn new(severity: Severity, message: impl Into<String>) -> Self {
101 Self {
102 severity,
103 message: message.into(),
104 file: None,
105 line: None,
106 column: None,
107 category: None,
108 fix_its: Vec::new(),
109 snippet: None,
110 note: None,
111 help: None,
112 }
113 }
114
115 pub fn error_category(&self) -> ErrorCategory {
117 if let Some(ref cat) = self.category {
118 let cat_lower = cat.to_lowercase();
119 if cat_lower.contains("parse") || cat_lower.contains("syntax") {
120 return ErrorCategory::Parse;
121 }
122 if cat_lower.contains("type") {
123 return ErrorCategory::Type;
124 }
125 if cat_lower.contains("semantic") {
126 return ErrorCategory::Semantic;
127 }
128 }
129
130 let msg = self.message.to_lowercase();
131 if msg.contains("expected") || msg.contains("unterminated") || msg.contains("extraneous") {
132 ErrorCategory::Parse
133 } else if msg.contains("incompatible") || msg.contains("implicit conversion") {
134 ErrorCategory::Type
135 } else if msg.contains("undeclared") || msg.contains("redefinition") {
136 ErrorCategory::Semantic
137 } else {
138 ErrorCategory::Parse
139 }
140 }
141
142 pub fn build_snippet(source: &str, line: u32, column: Option<u32>) -> Option<String> {
144 let lines: Vec<&str> = source.lines().collect();
145 let line_idx = line.checked_sub(1)? as usize;
146 if line_idx >= lines.len() {
147 return None;
148 }
149
150 let mut out = String::new();
151 let gutter_width = format!("{}", line_idx + 2).len().max(2);
152
153 if line_idx > 0 {
155 out.push_str(&format!(
156 "{:>width$}| {}\n",
157 line_idx,
158 lines[line_idx - 1],
159 width = gutter_width
160 ));
161 }
162
163 out.push_str(&format!(
165 "{:>width$}| {}\n",
166 line_idx + 1,
167 lines[line_idx],
168 width = gutter_width
169 ));
170
171 if let Some(col) = column {
173 let col_idx = (col as usize).saturating_sub(1);
174 let padding = " ".repeat(col_idx);
175 out.push_str(&format!(
176 "{:>width$}| {}^\n",
177 "",
178 padding,
179 width = gutter_width
180 ));
181 }
182
183 if line_idx + 1 < lines.len() {
185 out.push_str(&format!(
186 "{:>width$}| {}\n",
187 line_idx + 2,
188 lines[line_idx + 1],
189 width = gutter_width
190 ));
191 }
192
193 Some(out)
194 }
195
196 pub fn infer_note_and_help(&mut self) {
198 let msg = self.message.to_lowercase();
199
200 if msg.contains("expected ')'") {
201 self.note = Some("Unclosed parenthesis in expression or function call.".into());
202 self.help = Some("Add the missing ')' to close the expression.".into());
203 } else if msg.contains("expected ';'") {
204 self.note = Some("Missing semicolon after statement.".into());
205 self.help = Some("Add ';' at the end of the statement.".into());
206 } else if msg.contains("use of undeclared identifier")
207 || msg.contains("undeclared identifier")
208 {
209 self.note = Some("Variable or function not declared in this scope.".into());
210 self.help =
211 Some("Declare the variable before use, or check for typos in the name.".into());
212 } else if msg.contains("implicit declaration of function") {
213 self.note = Some("Function called before it is declared.".into());
214 self.help = Some(
215 "Add a #include for the header that declares this function, or add a forward declaration.".into(),
216 );
217 } else if msg.contains("incompatible") && msg.contains("type") {
218 self.note = Some("Type mismatch in assignment or return value.".into());
219 self.help =
220 Some("Ensure the types match, or add an explicit cast if intentional.".into());
221 } else if msg.contains("expected '}'") {
222 self.note = Some("Unclosed brace in block or struct definition.".into());
223 self.help = Some("Add the missing '}' to close the block.".into());
224 } else if msg.contains("redefinition of") {
225 self.note = Some("This name was already defined earlier in the same scope.".into());
226 self.help = Some("Rename one of the definitions, or use a different scope.".into());
227 } else if msg.contains("expected expression") {
228 self.note =
229 Some("The parser expected a value or expression but found something else.".into());
230 self.help = Some("Check for missing operands or misplaced punctuation.".into());
231 } else if !self.fix_its.is_empty() {
232 self.help = Some(self.fix_its.join("; "));
233 }
234 }
235}
236
237impl fmt::Display for Diagnostic {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 let category = self.error_category();
241 let header = format!("{}[{}]", self.severity.tag(), category);
242 let colored_header = match self.severity {
243 Severity::Error | Severity::Fatal => header.red().bold().to_string(),
244 Severity::Warning => header.yellow().bold().to_string(),
245 Severity::Note => header.cyan().bold().to_string(),
246 };
247 writeln!(f, "{}: {}", colored_header, self.message.bold())?;
248
249 if let Some(ref file) = self.file {
251 let loc = match (self.line, self.column) {
252 (Some(l), Some(c)) => format!("{}:{}:{}", file, l, c),
253 (Some(l), None) => format!("{}:{}", file, l),
254 _ => file.clone(),
255 };
256 writeln!(f, " {} {}", "-->".blue().bold(), loc)?;
257 }
258
259 if let Some(ref snippet) = self.snippet {
261 for line in snippet.lines() {
263 if let Some(pipe_pos) = line.find('|') {
264 let gutter = &line[..=pipe_pos];
265 let rest = &line[pipe_pos + 1..];
266 if rest.trim() == "^" || rest.contains('^') {
267 writeln!(f, " {}{}", gutter.blue(), rest.red())?;
269 } else {
270 writeln!(f, " {}{}", gutter.blue(), rest)?;
271 }
272 } else {
273 writeln!(f, " {}", line)?;
274 }
275 }
276 }
277
278 if let Some(ref note) = self.note {
280 writeln!(f, " {}: {}", "note".cyan().bold(), note)?;
281 }
282
283 if let Some(ref help) = self.help {
285 writeln!(f, " {}: {}", "help".green().bold(), help)?;
286 }
287
288 Ok(())
289 }
290}
291
292#[derive(Debug)]
297pub struct DiagnosticError {
298 pub diagnostics: Vec<Diagnostic>,
300}
301
302impl DiagnosticError {
303 pub fn new(diagnostics: Vec<Diagnostic>) -> Self {
305 Self { diagnostics }
306 }
307}
308
309impl fmt::Display for DiagnosticError {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 for diag in &self.diagnostics {
312 write!(f, "{}", diag)?;
313 }
314 let error_count = self
315 .diagnostics
316 .iter()
317 .filter(|d| d.severity >= Severity::Error)
318 .count();
319 if error_count > 0 {
320 write!(
321 f,
322 "{}",
323 format!(
324 "aborting due to {} previous error{}",
325 error_count,
326 if error_count == 1 { "" } else { "s" }
327 )
328 .red()
329 .bold()
330 )?;
331 }
332 Ok(())
333 }
334}
335
336impl std::error::Error for DiagnosticError {}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_severity_ordering() {
344 assert!(Severity::Note < Severity::Warning);
345 assert!(Severity::Warning < Severity::Error);
346 assert!(Severity::Error < Severity::Fatal);
347 }
348
349 #[test]
350 fn test_severity_tags() {
351 assert_eq!(Severity::Note.tag(), "note");
352 assert_eq!(Severity::Warning.tag(), "warning");
353 assert_eq!(Severity::Error.tag(), "error");
354 assert_eq!(Severity::Fatal.tag(), "fatal");
355 }
356
357 #[test]
358 fn test_error_category_from_clang_category() {
359 let mut d = Diagnostic::new(Severity::Error, "expected ')'");
360 d.category = Some("Parse Issue".into());
361 assert_eq!(d.error_category(), ErrorCategory::Parse);
362
363 d.category = Some("Semantic Issue".into());
364 assert_eq!(d.error_category(), ErrorCategory::Semantic);
365 }
366
367 #[test]
368 fn test_error_category_from_message() {
369 let d = Diagnostic::new(Severity::Error, "expected ';' after expression");
370 assert_eq!(d.error_category(), ErrorCategory::Parse);
371
372 let d = Diagnostic::new(Severity::Error, "use of undeclared identifier 'x'");
373 assert_eq!(d.error_category(), ErrorCategory::Semantic);
374
375 let d = Diagnostic::new(Severity::Error, "incompatible pointer types");
376 assert_eq!(d.error_category(), ErrorCategory::Type);
377 }
378
379 #[test]
380 fn test_display_contains_header() {
381 let d = Diagnostic::new(Severity::Error, "expected ')'");
382 let output = format!("{}", d);
383 assert!(output.contains("error[parse]"));
384 assert!(output.contains("expected ')'"));
385 }
386
387 #[test]
388 fn test_display_contains_location() {
389 let mut d = Diagnostic::new(Severity::Error, "expected ')'");
390 d.file = Some("test.c".into());
391 d.line = Some(15);
392 d.column = Some(22);
393 let output = format!("{}", d);
394 assert!(output.contains("-->"));
395 assert!(output.contains("test.c:15:22"));
396 }
397
398 #[test]
399 fn test_display_contains_snippet() {
400 let mut d = Diagnostic::new(Severity::Error, "expected ')'");
401 d.snippet = Some("14| int y = 10;\n15| int x = foo(bar;\n | ^\n16| return 0;\n".into());
402 let output = format!("{}", d);
403 assert!(output.contains("|"));
404 assert!(output.contains("^"));
405 }
406
407 #[test]
408 fn test_display_contains_note_and_help() {
409 let mut d = Diagnostic::new(Severity::Error, "expected ')'");
410 d.note = Some("Unclosed parenthesis.".into());
411 d.help = Some("Add ')' to close.".into());
412 let output = format!("{}", d);
413 assert!(output.contains("note:"));
414 assert!(output.contains("help:"));
415 assert!(output.contains("Unclosed parenthesis."));
416 assert!(output.contains("Add ')' to close."));
417 }
418
419 #[test]
420 fn test_build_snippet_middle_of_file() {
421 let source = "line 1\nline 2\nline 3\nline 4\nline 5";
422 let snippet = Diagnostic::build_snippet(source, 3, Some(4)).unwrap();
423 assert!(snippet.contains("line 2")); assert!(snippet.contains("line 3")); assert!(snippet.contains("line 4")); assert!(snippet.contains("^")); }
428
429 #[test]
430 fn test_build_snippet_first_line() {
431 let source = "int x = foo(bar;\nint y = 10;";
432 let snippet = Diagnostic::build_snippet(source, 1, Some(16)).unwrap();
433 assert!(snippet.contains("int x = foo(bar;"));
434 assert!(snippet.contains("^"));
435 }
437
438 #[test]
439 fn test_build_snippet_last_line() {
440 let source = "int y = 10;\nint x = foo(bar;";
441 let snippet = Diagnostic::build_snippet(source, 2, Some(16)).unwrap();
442 assert!(snippet.contains("int x = foo(bar;"));
443 assert!(snippet.contains("^"));
444 }
446
447 #[test]
448 fn test_build_snippet_no_column() {
449 let source = "line 1\nline 2\nline 3";
450 let snippet = Diagnostic::build_snippet(source, 2, None).unwrap();
451 assert!(snippet.contains("line 2"));
452 assert!(!snippet.contains("^")); }
454
455 #[test]
456 fn test_build_snippet_out_of_bounds() {
457 let source = "line 1\nline 2";
458 assert!(Diagnostic::build_snippet(source, 99, Some(1)).is_none());
459 assert!(Diagnostic::build_snippet(source, 0, Some(1)).is_none());
460 }
461
462 #[test]
463 fn test_infer_note_expected_paren() {
464 let mut d = Diagnostic::new(Severity::Error, "expected ')'");
465 d.infer_note_and_help();
466 assert!(d.note.is_some());
467 assert!(d.help.is_some());
468 assert!(d.note.unwrap().contains("parenthesis"));
469 }
470
471 #[test]
472 fn test_infer_note_expected_semicolon() {
473 let mut d = Diagnostic::new(Severity::Error, "expected ';' after expression");
474 d.infer_note_and_help();
475 assert!(d.note.unwrap().contains("semicolon"));
476 }
477
478 #[test]
479 fn test_infer_note_undeclared() {
480 let mut d = Diagnostic::new(Severity::Error, "use of undeclared identifier 'foo'");
481 d.infer_note_and_help();
482 assert!(d.note.unwrap().contains("not declared"));
483 }
484
485 #[test]
486 fn test_infer_note_falls_back_to_fix_its() {
487 let mut d = Diagnostic::new(Severity::Error, "some unusual error");
488 d.fix_its = vec!["insert ';'".into()];
489 d.infer_note_and_help();
490 assert!(d.help.unwrap().contains("insert ';'"));
491 }
492
493 #[test]
494 fn test_diagnostic_error_display() {
495 let d1 = Diagnostic::new(Severity::Error, "first error");
496 let d2 = Diagnostic::new(Severity::Error, "second error");
497 let err = DiagnosticError::new(vec![d1, d2]);
498 let output = format!("{}", err);
499 assert!(output.contains("first error"));
500 assert!(output.contains("second error"));
501 assert!(output.contains("2 previous errors"));
502 }
503
504 #[test]
505 fn test_diagnostic_error_single() {
506 let d = Diagnostic::new(Severity::Error, "only error");
507 let err = DiagnosticError::new(vec![d]);
508 let output = format!("{}", err);
509 assert!(output.contains("1 previous error"));
510 assert!(!output.contains("1 previous errors"));
512 }
513
514 #[test]
515 fn test_diagnostic_error_is_std_error() {
516 let err: Box<dyn std::error::Error> =
517 Box::new(DiagnosticError::new(vec![Diagnostic::new(
518 Severity::Error,
519 "test",
520 )]));
521 assert!(!err.to_string().is_empty());
522 }
523
524 #[test]
525 fn test_warning_severity_display() {
526 let d = Diagnostic::new(Severity::Warning, "implicit conversion loses precision");
527 let output = format!("{}", d);
528 assert!(output.contains("warning"));
529 }
530}