1use crate::lexer::Span;
10use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Severity {
15 Error,
17 Warning,
19}
20
21#[derive(Debug, Clone)]
23pub struct Diagnostic {
24 pub severity: Severity,
26 pub span: Span,
28 pub message: String,
30 pub hint: Option<String>,
32 pub source_line: Option<String>,
34}
35
36impl Diagnostic {
37 pub fn error(span: Span, message: impl Into<String>) -> Self {
39 Self {
40 severity: Severity::Error,
41 span,
42 message: message.into(),
43 hint: None,
44 source_line: None,
45 }
46 }
47
48 pub fn warning(span: Span, message: impl Into<String>) -> Self {
50 Self {
51 severity: Severity::Warning,
52 span,
53 message: message.into(),
54 hint: None,
55 source_line: None,
56 }
57 }
58
59 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
61 self.hint = Some(hint.into());
62 self
63 }
64
65 pub fn with_source_line(mut self, line: impl Into<String>) -> Self {
67 self.source_line = Some(line.into());
68 self
69 }
70}
71
72impl fmt::Display for Diagnostic {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 let severity = match self.severity {
75 Severity::Error => "error",
76 Severity::Warning => "warning",
77 };
78 write!(
79 f,
80 "{}:{}:{}: {}",
81 self.span.line, self.span.col, severity, self.message
82 )?;
83
84 if let Some(ref line) = self.source_line {
85 write!(f, "\n | {line}")?;
86 if self.span.col > 0 {
88 let padding = " ".repeat(self.span.col - 1);
89 write!(f, "\n | {padding}^")?;
90 }
91 }
92
93 if let Some(ref hint) = self.hint {
94 write!(f, "\n = hint: {hint}")?;
95 }
96
97 Ok(())
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct DiagnosticReport {
104 pub diagnostics: Vec<Diagnostic>,
106 source_lines: Vec<String>,
108}
109
110impl DiagnosticReport {
111 pub fn new(source: &str) -> Self {
113 Self {
114 diagnostics: Vec::new(),
115 source_lines: source.lines().map(|l| l.to_string()).collect(),
116 }
117 }
118
119 pub fn error(&mut self, span: Span, message: impl Into<String>) {
121 let mut diag = Diagnostic::error(span, message);
122 if span.line > 0 && span.line <= self.source_lines.len() {
123 diag.source_line = Some(self.source_lines[span.line - 1].clone());
124 }
125 self.diagnostics.push(diag);
126 }
127
128 pub fn error_with_hint(
130 &mut self,
131 span: Span,
132 message: impl Into<String>,
133 hint: impl Into<String>,
134 ) {
135 let mut diag = Diagnostic::error(span, message).with_hint(hint);
136 if span.line > 0 && span.line <= self.source_lines.len() {
137 diag.source_line = Some(self.source_lines[span.line - 1].clone());
138 }
139 self.diagnostics.push(diag);
140 }
141
142 pub fn warning(&mut self, span: Span, message: impl Into<String>) {
144 let mut diag = Diagnostic::warning(span, message);
145 if span.line > 0 && span.line <= self.source_lines.len() {
146 diag.source_line = Some(self.source_lines[span.line - 1].clone());
147 }
148 self.diagnostics.push(diag);
149 }
150
151 pub fn warning_with_hint(
153 &mut self,
154 span: Span,
155 message: impl Into<String>,
156 hint: impl Into<String>,
157 ) {
158 let mut diag = Diagnostic::warning(span, message).with_hint(hint);
159 if span.line > 0 && span.line <= self.source_lines.len() {
160 diag.source_line = Some(self.source_lines[span.line - 1].clone());
161 }
162 self.diagnostics.push(diag);
163 }
164
165 pub fn has_errors(&self) -> bool {
167 self.diagnostics
168 .iter()
169 .any(|d| d.severity == Severity::Error)
170 }
171
172 pub fn errors(&self) -> Vec<&Diagnostic> {
174 self.diagnostics
175 .iter()
176 .filter(|d| d.severity == Severity::Error)
177 .collect()
178 }
179
180 pub fn warnings(&self) -> Vec<&Diagnostic> {
182 self.diagnostics
183 .iter()
184 .filter(|d| d.severity == Severity::Warning)
185 .collect()
186 }
187}
188
189impl fmt::Display for DiagnosticReport {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 for (i, diag) in self.diagnostics.iter().enumerate() {
192 if i > 0 {
193 writeln!(f)?;
194 }
195 write!(f, "{diag}")?;
196 }
197 Ok(())
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn diagnostic_display() {
207 let diag = Diagnostic::error(Span { line: 3, col: 12 }, "unknown function 'foobar'")
208 .with_hint("did you mean 'hash'?")
209 .with_source_line(" result := foobar(cycle)");
210 let s = diag.to_string();
211 assert!(s.contains("3:12:error"));
212 assert!(s.contains("unknown function"));
213 assert!(s.contains("foobar(cycle)"));
214 assert!(s.contains("did you mean"));
215 }
216
217 #[test]
218 fn report_collects() {
219 let mut report = DiagnosticReport::new("line1\nline2\nline3");
220 report.error(Span { line: 1, col: 1 }, "first error");
221 report.warning(Span { line: 2, col: 5 }, "a warning");
222 report.error(Span { line: 3, col: 1 }, "second error");
223 assert!(report.has_errors());
224 assert_eq!(report.errors().len(), 2);
225 assert_eq!(report.warnings().len(), 1);
226 }
227
228 #[test]
229 fn report_includes_source_line() {
230 let mut report = DiagnosticReport::new("input cycle: u64\nbad := ???");
231 report.error(Span { line: 2, col: 8 }, "unexpected token");
232 let s = report.to_string();
233 assert!(s.contains("bad := ???"));
234 }
235}