1use std::fmt;
2
3use crate::{LineCol, Source, Span};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct YamlError {
8 pub diagnostic: Diagnostic,
10}
11
12impl YamlError {
13 #[must_use]
15 pub const fn new(diagnostic: Diagnostic) -> Self {
16 Self { diagnostic }
17 }
18
19 #[must_use]
22 pub fn with_position_from(mut self, source: &Source) -> Self {
23 if self.diagnostic.position.is_none() {
24 self.diagnostic.position = Some(source.diagnostic_position(&self.diagnostic));
25 }
26 self
27 }
28
29 #[must_use]
31 pub fn render<'a>(&'a self, source: &'a str) -> DiagnosticRenderer<'a> {
32 self.diagnostic.render(source)
33 }
34}
35
36impl fmt::Display for YamlError {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 self.diagnostic.fmt(formatter)
39 }
40}
41
42impl std::error::Error for YamlError {}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Diagnostic {
47 pub kind: DiagnosticKind,
49 pub message: String,
51 pub span: Span,
53 pub position: Option<LineCol>,
55 pub expected: Vec<String>,
57 pub notes: Vec<String>,
59}
60
61impl Diagnostic {
62 #[must_use]
64 pub fn new(kind: DiagnosticKind, message: impl Into<String>, span: Span) -> Self {
65 Self {
66 kind,
67 message: message.into(),
68 span,
69 position: None,
70 expected: Vec::new(),
71 notes: Vec::new(),
72 }
73 }
74
75 #[must_use]
77 pub const fn with_position(mut self, position: LineCol) -> Self {
78 self.position = Some(position);
79 self
80 }
81
82 #[must_use]
84 pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
85 self.expected.push(expected.into());
86 self
87 }
88
89 #[must_use]
91 pub fn with_note(mut self, note: impl Into<String>) -> Self {
92 self.notes.push(note.into());
93 self
94 }
95
96 #[must_use]
98 pub const fn render<'a>(&'a self, source: &'a str) -> DiagnosticRenderer<'a> {
99 DiagnosticRenderer {
100 diagnostic: self,
101 source,
102 source_name: None,
103 color: DiagnosticColor::Never,
104 }
105 }
106}
107
108impl fmt::Display for Diagnostic {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(formatter, "{:?}: {}", self.kind, self.message)?;
111
112 if let Some(position) = self.position {
113 write!(formatter, " at {}:{}", position.line, position.column)?;
114 }
115
116 if !self.expected.is_empty() {
117 write!(formatter, " (expected: {})", self.expected.join(", "))?;
118 }
119
120 for note in &self.notes {
121 write!(formatter, "\nnote: {note}")?;
122 }
123
124 Ok(())
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum DiagnosticKind {
131 Source,
133 Lexer,
135 Parser,
137 Semantic,
139 Typed,
141 Emitter,
143}
144
145impl DiagnosticKind {
146 const fn label(self) -> &'static str {
147 match self {
148 Self::Source => "source",
149 Self::Lexer => "lexer",
150 Self::Parser => "parser",
151 Self::Semantic => "semantic",
152 Self::Typed => "typed",
153 Self::Emitter => "emitter",
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
160pub enum DiagnosticColor {
161 #[default]
163 Never,
164 Always,
166}
167
168#[derive(Debug, Clone, Copy)]
170pub struct DiagnosticRenderer<'a> {
171 diagnostic: &'a Diagnostic,
172 source: &'a str,
173 source_name: Option<&'a str>,
174 color: DiagnosticColor,
175}
176
177impl<'a> DiagnosticRenderer<'a> {
178 #[must_use]
180 pub const fn with_source_name(mut self, source_name: &'a str) -> Self {
181 self.source_name = Some(source_name);
182 self
183 }
184
185 #[must_use]
187 pub const fn with_color(mut self, color: DiagnosticColor) -> Self {
188 self.color = color;
189 self
190 }
191
192 fn styled(
193 &self,
194 formatter: &mut fmt::Formatter<'_>,
195 code: &str,
196 value: impl fmt::Display,
197 ) -> fmt::Result {
198 if self.color == DiagnosticColor::Always {
199 write!(formatter, "\x1b[{code}m{value}\x1b[0m")
200 } else {
201 write!(formatter, "{value}")
202 }
203 }
204}
205
206impl fmt::Display for DiagnosticRenderer<'_> {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 self.styled(formatter, "1;31", "error")?;
209 write!(
210 formatter,
211 "[{}]: {}",
212 self.diagnostic.kind.label(),
213 self.diagnostic.message
214 )?;
215
216 let source_len = self.source.len();
217 let start = (self.diagnostic.span.start as usize).min(source_len);
218 let requested_end = self.diagnostic.span.end as usize;
219 let end = requested_end.max(start).min(source_len);
220 if !self.source.is_char_boundary(start) || !self.source.is_char_boundary(end) {
221 return write!(formatter, "\n{}", self.diagnostic);
222 }
223
224 let starts = line_starts(self.source);
225 let line_index = starts
226 .partition_point(|offset| *offset <= start)
227 .saturating_sub(1);
228 let line_start = starts[line_index];
229 let line_end = source_line_end(self.source, &starts, line_index);
230 let position = LineCol {
231 line: line_index + 1,
232 column: display_width(&self.source[line_start..start]) + 1,
233 };
234
235 write!(formatter, "\n ")?;
236 self.styled(formatter, "1;34", "-->")?;
237 write!(
238 formatter,
239 " {}:{}:{}",
240 self.source_name.unwrap_or("<input>"),
241 position.line,
242 position.column
243 )?;
244
245 let first_shown = line_index.saturating_sub(1);
246 let width = (line_index + 1).to_string().len();
247 write!(formatter, "\n{:width$} ", "", width = width)?;
248 self.styled(formatter, "1;34", "|")?;
249 for shown in first_shown..=line_index {
250 let shown_start = starts[shown];
251 let shown_end = source_line_end(self.source, &starts, shown);
252 let text = expand_tabs(&self.source[shown_start..shown_end]);
253 write!(formatter, "\n{:>width$} ", shown + 1, width = width)?;
254 self.styled(formatter, "1;34", "|")?;
255 write!(formatter, " {text}")?;
256 }
257
258 let prefix_width = display_width(&self.source[line_start..start]);
259 let underline_end = end.min(line_end);
260 let underline_width = if underline_end > start {
261 display_width(&self.source[start..underline_end]).max(1)
262 } else {
263 1
264 };
265 write!(formatter, "\n{:width$} ", "", width = width)?;
266 self.styled(formatter, "1;34", "|")?;
267 write!(formatter, " {}", " ".repeat(prefix_width))?;
268 self.styled(formatter, "1;31", "^".repeat(underline_width))?;
269 if !self.diagnostic.expected.is_empty() {
270 write!(
271 formatter,
272 " expected {}",
273 self.diagnostic.expected.join(", ")
274 )?;
275 }
276
277 let end_line = starts
278 .partition_point(|offset| *offset <= end)
279 .saturating_sub(1);
280 if end_line > line_index || requested_end > source_len {
281 write!(formatter, "\n{:width$} ", "", width = width)?;
282 self.styled(formatter, "1;34", "|")?;
283 write!(formatter, " ... span continues")?;
284 }
285 write!(formatter, "\n{:width$} ", "", width = width)?;
286 self.styled(formatter, "1;34", "|")?;
287
288 for note in &self.diagnostic.notes {
289 writeln!(formatter)?;
290 self.styled(formatter, "1;32", "note")?;
291 write!(formatter, ": {note}")?;
292 }
293 Ok(())
294 }
295}
296
297fn line_starts(source: &str) -> Vec<usize> {
298 let mut starts = vec![0];
299 starts.extend(
300 source
301 .bytes()
302 .enumerate()
303 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
304 );
305 starts
306}
307
308fn source_line_end(source: &str, starts: &[usize], line_index: usize) -> usize {
309 let mut end = starts.get(line_index + 1).copied().unwrap_or(source.len());
310 if end > starts[line_index] && source.as_bytes()[end - 1] == b'\n' {
311 end -= 1;
312 }
313 if end > starts[line_index] && source.as_bytes()[end - 1] == b'\r' {
314 end -= 1;
315 }
316 end
317}
318
319fn display_width(text: &str) -> usize {
320 text.chars().fold(0, |width, character| match character {
321 '\t' => width + (4 - width % 4),
322 _ => width + 1,
323 })
324}
325
326fn expand_tabs(text: &str) -> String {
327 let mut expanded = String::with_capacity(text.len());
328 let mut width = 0;
329 for character in text.chars() {
330 if character == '\t' {
331 let spaces = 4 - width % 4;
332 expanded.push_str(&" ".repeat(spaces));
333 width += spaces;
334 } else {
335 expanded.push(character);
336 width += 1;
337 }
338 }
339 expanded
340}
341
342pub type ParseError = YamlError;