luaparse/
error.rs

1//! Fancy error printing.
2//!
3//! ```
4//! # use luaparse::{error::Error, Span, Position};
5//! let buf = "a = 42";
6//! let error = Error::new(
7//!     Span {
8//!         start: Position {
9//!             byte: 0,
10//!             line: 1,
11//!             col: 1,
12//!         },
13//!         end: Position {
14//!             byte: 0,
15//!             line: 1,
16//!             col: 1,
17//!         }
18//!     },
19//!     "unfavorable planet alignment",
20//! );
21//! let e = error.with_buffer(buf);
22//!
23//! // Plain error printing
24//! println!("{}", e);
25//!
26//! // Fancy error printing
27//! println!("{:#}", e);
28//! ```
29
30use std::error::Error as ErrorTrait;
31use std::fmt::{self, Display};
32
33use colored::{Color, Colorize};
34use unicode_width::UnicodeWidthStr;
35
36use crate::cursor::InputCursor;
37use crate::lexer::Lexer;
38use crate::span::Span;
39use crate::symbol::SymbolGroup;
40use crate::token::{Token, TokenKind, TokenValue};
41
42struct LineIter<'a> {
43    s: &'a str,
44    eof: bool,
45}
46
47impl<'a> LineIter<'a> {
48    fn new(s: &'a str) -> Self {
49        Self { s, eof: false }
50    }
51}
52
53impl<'a> Iterator for LineIter<'a> {
54    type Item = &'a str;
55
56    fn next(&mut self) -> Option<Self::Item> {
57        if self.eof {
58            return None;
59        }
60
61        let mut iter = self.s.chars().peekable();
62        let mut line = "";
63        self.eof = true;
64
65        while let Some(c) = iter.next() {
66            match c {
67                '\r' if iter.peek() == Some(&'\n') => self.s = &self.s[(line.len() + 2)..],
68                '\n' if iter.peek() == Some(&'\r') => self.s = &self.s[(line.len() + 2)..],
69                '\r' | '\n' => self.s = &self.s[(line.len() + 1)..],
70                _ => line = &self.s[..(line.len() + c.len_utf8())],
71            }
72
73            if c == '\r' || c == '\n' {
74                self.eof = false;
75                return Some(line);
76            }
77        }
78
79        self.s = &self.s[line.len()..];
80
81        Some(line)
82    }
83}
84
85/// The severity of the error.
86#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
87pub enum Severity {
88    Error,
89    Warning,
90}
91
92impl Display for Severity {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let s = match self {
95            Self::Error => "error",
96            Self::Warning => "warning",
97        };
98
99        if formatter.alternate() {
100            let s = match self {
101                Self::Error => s.bright_red(),
102                Self::Warning => s.yellow(),
103            }.bold();
104
105            write!(formatter, "{}", s)
106        } else {
107            write!(formatter, "{}", s)
108        }
109    }
110}
111
112impl Default for Severity {
113    fn default() -> Self {
114        Self::Error
115    }
116}
117
118/// The error with an associated span.
119#[derive(Clone, Debug, Eq, Hash, PartialEq)]
120pub struct Error {
121    span: Span,
122    severity: Severity,
123    message: String,
124}
125
126impl Display for Error {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        if !formatter.alternate() {
129            write!(formatter, "{}", self.message)
130        } else {
131            write!(
132                formatter,
133                "{:#}: {} ({})",
134                self.severity, self.message, self.span
135            )
136        }
137    }
138}
139
140impl ErrorTrait for Error {}
141
142impl Error {
143    /// Create a new `Error`.
144    pub fn new(span: Span, message: impl Display) -> Self {
145        Self {
146            span,
147            severity: Default::default(),
148            message: message.to_string(),
149        }
150    }
151
152    /// Set the severity.
153    pub fn with_severity(self, severity: Severity) -> Self {
154        Self { severity, ..self }
155    }
156
157    /// Create an `ErrorPrinter` for fancy error printing.
158    ///
159    /// `buf` is the source code buffer.
160    pub fn with_buffer<'a>(&'a self, buf: &'a str) -> ErrorPrinter<'a> {
161        ErrorPrinter { error: &self, buf }
162    }
163
164    /// Get the error span.
165    pub fn span(&self) -> Span {
166        self.span
167    }
168
169    /// Get the error severity.
170    pub fn severity(&self) -> Severity {
171        self.severity
172    }
173}
174
175/// A wrapper type encompassing an `Error` and the source buffer.
176#[derive(Debug)]
177pub struct ErrorPrinter<'a> {
178    error: &'a Error,
179    buf: &'a str,
180}
181
182impl Display for ErrorPrinter<'_> {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        if formatter.alternate() {
185            writeln!(formatter, "{:#}", self.error)?;
186            display_source_for_span(formatter, self.error.span, self.buf)
187        } else {
188            write!(formatter, "{}", self.error)
189        }
190    }
191}
192
193fn get_color_for_token(token: &Token<'_>) -> Option<Color> {
194    match token.kind() {
195        TokenKind::Number => Some(Color::BrightCyan),
196        TokenKind::String => Some(Color::Yellow),
197        TokenKind::Comment => Some(Color::BrightGreen),
198        TokenKind::Whitespace => None,
199        TokenKind::Ident => None,
200        TokenKind::Eof => None,
201        TokenKind::Symbol(s) => match s.group() {
202            SymbolGroup::Operator => Some(Color::Cyan),
203            SymbolGroup::Keyword => Some(Color::Magenta),
204            SymbolGroup::Value => Some(Color::BrightCyan),
205        },
206    }
207}
208
209fn line_range_to_show(Span { start, end }: Span) -> (u32, u32) {
210    let lines = end.line - start.line + 1;
211
212    match lines {
213        1 => (start.line.checked_sub(2).unwrap_or(1).max(1), end.line + 2),
214        2 => ((start.line - 1).max(1), end.line + 2),
215        _ => ((start.line - 1).max(1), end.line + 1),
216    }
217}
218
219fn start_new_line(f: &mut fmt::Formatter<'_>, current_line: u32, width: usize) -> fmt::Result {
220    write!(f, " {:>width$} │  ", current_line, width = width)
221}
222
223fn print_line_skip(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
224    write!(f, "╶{:╴>width$}┼╴", "─", width = width + 1)
225}
226
227fn print_blank_margin(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
228    write!(f, " {:>width$}", " ", width = width + 1)
229}
230
231fn print_first_line(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
232    print_blank_margin(f, width)?;
233    writeln!(f, "┌")
234}
235
236fn print_last_line(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
237    print_blank_margin(f, width)?;
238    writeln!(f, "└")
239}
240
241fn print_line_features(
242    f: &mut fmt::Formatter<'_>,
243    current_line: u32,
244    line: &str,
245    width: usize,
246    span: Span,
247) -> fmt::Result {
248    if current_line == span.start.line {
249        let leading_pos: usize = line
250            .chars()
251            .take((span.start.col - 1) as usize)
252            .map(char::len_utf8)
253            .sum();
254        let leading_spaces = line[..leading_pos].width();
255        print_blank_margin(f, width)?;
256        write!(f, "│  ")?;
257
258        if span.start.line == span.end.line {
259            let n = span.end.col - span.start.col + 1;
260            let len: usize = line[leading_pos..]
261                .chars()
262                .take(n as usize)
263                .map(char::len_utf8)
264                .sum();
265            let width = line[leading_pos..(leading_pos + len)].width();
266
267            match width {
268                0 => writeln!(f, "{:>col$}", "↖", col = leading_spaces + 1)?,
269                1 => writeln!(f, "{:>col$}", "─", col = leading_spaces + 1)?,
270                2 => writeln!(f, "{:>col$}┘", "└", col = leading_spaces + 1)?,
271                _ => writeln!(
272                    f,
273                    "{:>col$}{:─>width$}┘",
274                    "└",
275                    "─",
276                    col = leading_spaces + 1,
277                    width = width - 2
278                )?,
279            }
280        } else {
281            let width = line[leading_pos..].width().checked_sub(1);
282
283            match width {
284                None => writeln!(f, "{:>width$}╶╶", "└", width = line.width() + 1)?,
285                Some(0) => writeln!(f, "{:>col$}╶╶", "└", col = leading_spaces + 1)?,
286                Some(w) => writeln!(
287                    f,
288                    "{:>col$}{:─>width$}╶╶",
289                    "└",
290                    "─",
291                    col = leading_spaces + 1,
292                    width = w
293                )?,
294            }
295        }
296    } else if current_line == span.end.line {
297        print_blank_margin(f, width)?;
298        write!(f, "│╶╶")?;
299        let len = line.chars().take(span.end.col as usize).map(char::len_utf8).sum();
300        let width = line[..len].width();
301
302        match width {
303            0 => writeln!(f, "{:>col$}", "┘", col = width)?,
304            _ => writeln!(f, "{:─>width$}┘", "─", width = width - 1)?,
305        }
306    }
307
308    Ok(())
309}
310
311fn num_digits(n: u32) -> usize {
312    ((n as f64).log10() + 1f64) as usize
313}
314
315fn remove_trailing_newline(s: &str) -> &str {
316    let tail = match s.get((s.len() - 2)..) {
317        Some(tail) => tail,
318        None => return s,
319    };
320
321    if tail == "\r\n" || tail == "\n\r" {
322        &s[..(s.len() - 2)]
323    } else if &tail[1..] == "\r" || &tail[1..] == "\n" {
324        &s[..(s.len() - 1)]
325    } else {
326        s
327    }
328}
329
330fn display_source_for_span(f: &mut fmt::Formatter<'_>, span: Span, buf: &str) -> fmt::Result {
331    let buf = remove_trailing_newline(buf);
332    let mut lines = LineIter::new(buf);
333    let (start, end) = line_range_to_show(span);
334    let width = num_digits(end);
335    let cursor = InputCursor::new(buf);
336    let mut pos = cursor.pos();
337    let mut lexer = Lexer::new(cursor);
338    let mut current_line = 1;
339    let mut skip_written = false;
340    let write_skips = end - start + 1 > 5;
341
342    print_first_line(f, width)?;
343    start_new_line(f, start, width)?;
344
345    loop {
346        let token = match lexer.next() {
347            Some(Err(_)) => break,
348            None | Some(Ok(Token { value: TokenValue::Eof, .. })) => {
349                // the last line is not yet finished
350                writeln!(f)?;
351                print_line_features(f, current_line, lines.next().unwrap(), width, span)?;
352                print_last_line(f, width)?;
353                return Ok(());
354            }
355            Some(Ok(token)) => token,
356        };
357
358        pos = lexer.cursor().pos();
359
360        if token.span.end.line < start {
361            if token.span.end.line != current_line {
362                for _ in current_line..token.span.end.line {
363                    lines.next();
364                }
365
366                current_line = token.span.end.line;
367            }
368
369            continue;
370        }
371
372        let color = get_color_for_token(&token);
373        let mut first_line = true;
374
375        for (i, line) in
376            LineIter::new(lexer.cursor().substr(token.span.start..=token.span.end)).enumerate()
377        {
378            if i != 0 {
379                current_line += 1;
380            }
381
382            if current_line < start {
383                lines.next();
384                continue;
385            }
386
387            if current_line > start + 1 && current_line < end - 1 && write_skips {
388                if !skip_written {
389                    writeln!(f)?;
390                    print_line_skip(f, width)?;
391                    skip_written = true;
392                }
393
394                continue;
395            }
396
397            if !first_line {
398                writeln!(f)?;
399            }
400
401            if current_line > end {
402                print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
403                print_last_line(f, width)?;
404                return Ok(());
405            }
406
407            if !first_line {
408                print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
409                start_new_line(f, current_line, width)?;
410            }
411
412            match color {
413                Some(color) => write!(f, "{}", line.color(color))?,
414                None => write!(f, "{}", line)?,
415            }
416
417            first_line = false;
418        }
419    }
420
421    let remaining = lexer.cursor().substr(pos..);
422
423    for (i, line) in LineIter::new(remaining).enumerate() {
424        if current_line < start {
425            lines.next();
426            continue;
427        }
428
429        if current_line > end {
430            break;
431        }
432
433        if current_line > start + 1 && current_line < end - 1 && write_skips {
434            if !skip_written {
435                print_line_skip(f, width)?;
436                skip_written = true;
437            }
438
439            lines.next();
440            continue;
441        }
442
443        if i != 0 {
444            print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
445            start_new_line(f, current_line, width)?;
446        }
447
448        writeln!(f, "{}", line)?;
449
450        current_line += 1;
451    }
452
453    print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
454    print_last_line(f, width)?;
455    Ok(())
456}