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 label: None,
105 }
106 }
107}
108
109impl fmt::Display for Diagnostic {
110 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(formatter, "{:?}: {}", self.kind, self.message)?;
112
113 if let Some(position) = self.position {
114 write!(formatter, " at {}:{}", position.line, position.column)?;
115 }
116
117 if !self.expected.is_empty() {
118 write!(formatter, " (expected: {})", self.expected.join(", "))?;
119 }
120
121 for note in &self.notes {
122 write!(formatter, "\nnote: {note}")?;
123 }
124
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum DiagnosticKind {
132 Source,
134 Lexer,
136 Parser,
138 Semantic,
140 Typed,
142 Emitter,
144}
145
146impl DiagnosticKind {
147 const fn label(self) -> &'static str {
148 match self {
149 Self::Source => "source",
150 Self::Lexer => "lexer",
151 Self::Parser => "parser",
152 Self::Semantic => "semantic",
153 Self::Typed => "typed",
154 Self::Emitter => "emitter",
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
161pub enum DiagnosticColor {
162 #[default]
164 Never,
165 Always,
167}
168
169#[derive(Debug, Clone, Copy)]
171pub struct DiagnosticRenderer<'a> {
172 diagnostic: &'a Diagnostic,
173 source: &'a str,
174 source_name: Option<&'a str>,
175 color: DiagnosticColor,
176 label: Option<&'a str>,
177}
178
179impl<'a> DiagnosticRenderer<'a> {
180 #[must_use]
182 pub const fn with_label(mut self, label: &'a str) -> Self {
183 self.label = Some(label);
184 self
185 }
186 #[must_use]
188 pub const fn with_source_name(mut self, source_name: &'a str) -> Self {
189 self.source_name = Some(source_name);
190 self
191 }
192
193 #[must_use]
195 pub const fn with_color(mut self, color: DiagnosticColor) -> Self {
196 self.color = color;
197 self
198 }
199
200 fn styled(
201 &self,
202 formatter: &mut fmt::Formatter<'_>,
203 code: &str,
204 value: impl fmt::Display,
205 ) -> fmt::Result {
206 if self.color == DiagnosticColor::Always {
207 write!(formatter, "\x1b[{code}m{value}\x1b[0m")
208 } else {
209 write!(formatter, "{value}")
210 }
211 }
212}
213
214impl fmt::Display for DiagnosticRenderer<'_> {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 self.styled(formatter, "1;31", "error")?;
217 write!(
218 formatter,
219 "[{}]: {}",
220 self.label.unwrap_or(self.diagnostic.kind.label()),
221 self.diagnostic.message
222 )?;
223
224 let source_len = self.source.len();
225 let start = (self.diagnostic.span.start as usize).min(source_len);
226 let requested_end = self.diagnostic.span.end as usize;
227 let end = requested_end.max(start).min(source_len);
228 if !self.source.is_char_boundary(start) || !self.source.is_char_boundary(end) {
229 return write!(formatter, "\n{}", self.diagnostic);
230 }
231
232 let starts = line_starts(self.source);
233 let line_index = starts
234 .partition_point(|offset| *offset <= start)
235 .saturating_sub(1);
236 let line_start = starts[line_index];
237 let line_end = source_line_end(self.source, &starts, line_index);
238 let position = LineCol {
239 line: line_index + 1,
240 column: display_width(&self.source[line_start..start]) + 1,
241 };
242
243 write!(formatter, "\n ")?;
244 self.styled(formatter, "1;34", "-->")?;
245 write!(
246 formatter,
247 " {}:{}:{}",
248 self.source_name.unwrap_or("<input>"),
249 position.line,
250 position.column
251 )?;
252
253 let first_shown = line_index.saturating_sub(1);
254 let width = (line_index + 1).to_string().len();
255 write!(formatter, "\n{:width$} ", "", width = width)?;
256 self.styled(formatter, "1;34", "|")?;
257 for shown in first_shown..=line_index {
258 let shown_start = starts[shown];
259 let shown_end = source_line_end(self.source, &starts, shown);
260 let text = expand_tabs(&self.source[shown_start..shown_end]);
261 write!(formatter, "\n{:>width$} ", shown + 1, width = width)?;
262 self.styled(formatter, "1;34", "|")?;
263 write!(formatter, " {text}")?;
264 }
265
266 let prefix_width = display_width(&self.source[line_start..start]);
267 let underline_end = end.min(line_end);
268 let underline_width = if underline_end > start {
269 display_width(&self.source[start..underline_end]).max(1)
270 } else {
271 1
272 };
273 write!(formatter, "\n{:width$} ", "", width = width)?;
274 self.styled(formatter, "1;34", "|")?;
275 write!(formatter, " {}", " ".repeat(prefix_width))?;
276 self.styled(formatter, "1;31", "^".repeat(underline_width))?;
277 if !self.diagnostic.expected.is_empty() {
278 write!(
279 formatter,
280 " expected {}",
281 self.diagnostic.expected.join(", ")
282 )?;
283 }
284
285 let end_line = starts
286 .partition_point(|offset| *offset <= end)
287 .saturating_sub(1);
288 if end_line > line_index || requested_end > source_len {
289 write!(formatter, "\n{:width$} ", "", width = width)?;
290 self.styled(formatter, "1;34", "|")?;
291 write!(formatter, " ... span continues")?;
292 }
293 write!(formatter, "\n{:width$} ", "", width = width)?;
294 self.styled(formatter, "1;34", "|")?;
295
296 for note in &self.diagnostic.notes {
297 writeln!(formatter)?;
298 self.styled(formatter, "1;32", "note")?;
299 write!(formatter, ": {note}")?;
300 }
301 Ok(())
302 }
303}
304
305fn line_starts(source: &str) -> Vec<usize> {
306 let mut starts = vec![0];
307 starts.extend(
308 source
309 .bytes()
310 .enumerate()
311 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
312 );
313 starts
314}
315
316fn source_line_end(source: &str, starts: &[usize], line_index: usize) -> usize {
317 let mut end = starts.get(line_index + 1).copied().unwrap_or(source.len());
318 if end > starts[line_index] && source.as_bytes()[end - 1] == b'\n' {
319 end -= 1;
320 }
321 if end > starts[line_index] && source.as_bytes()[end - 1] == b'\r' {
322 end -= 1;
323 }
324 end
325}
326
327fn display_width(text: &str) -> usize {
328 text.chars().fold(0, |width, character| match character {
329 '\t' => width + (4 - width % 4),
330 _ => width + 1,
331 })
332}
333
334fn expand_tabs(text: &str) -> String {
335 let mut expanded = String::with_capacity(text.len());
336 let mut width = 0;
337 for character in text.chars() {
338 if character == '\t' {
339 let spaces = 4 - width % 4;
340 expanded.push_str(&" ".repeat(spaces));
341 width += spaces;
342 } else {
343 expanded.push(character);
344 width += 1;
345 }
346 }
347 expanded
348}
349
350pub type ParseError = YamlError;