bulloak_syntax/
error.rs

1use std::{cmp, fmt};
2
3use crate::{span::Span, utils::repeat_str};
4
5/// A trait for representing frontend errors in the `bulloak-syntax` crate.
6///
7/// This trait is implemented by various error types in the crate to provide
8/// a consistent interface for error handling and formatting.
9pub trait FrontendError<K: fmt::Display>: std::error::Error {
10    /// Return the type of this error.
11    #[must_use]
12    fn kind(&self) -> &K;
13
14    /// The original text string in which this error occurred.
15    #[must_use]
16    fn text(&self) -> &str;
17
18    /// Return the span at which this error occurred.
19    #[must_use]
20    fn span(&self) -> &Span;
21
22    /// Formats the error message with additional context.
23    ///
24    /// This method provides a default implementation that creates a formatted
25    /// error message including the error kind, the relevant text, and visual
26    /// indicators of where the error occurred.
27    ///
28    /// # Arguments
29    /// * `f` - A mutable reference to a `fmt::Formatter`.
30    ///
31    /// # Returns
32    /// A `fmt::Result` indicating whether the formatting was successful.
33    fn format_error(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
34        let divider = repeat_str("•", 79);
35        writeln!(f, "{divider}")?;
36
37        let start_offset = self.span().start.offset;
38        let end_offset = self.span().end.offset;
39        if start_offset == end_offset && start_offset == 0 {
40            write!(f, "bulloak error: {}", self.kind())?;
41            return Ok(());
42        }
43
44        writeln!(f, "bulloak error: {}\n", self.kind())?;
45        let notated = self.notate();
46        writeln!(f, "{notated}")?;
47        writeln!(
48            f,
49            "--- (line {}, column {}) ---",
50            self.span().start.line,
51            self.span().start.column
52        )?;
53        Ok(())
54    }
55
56    /// Creates a string with carets (^) pointing at the span where the error
57    /// occurred.
58    ///
59    /// This method provides a visual representation of where in the text the
60    /// error was found.
61    ///
62    /// # Returns
63    /// A `String` containing the relevant line of text with carets underneath.
64    fn notate(&self) -> String {
65        let mut notated = String::new();
66        if let Some(line) = self.text().lines().nth(self.span().start.line - 1)
67        {
68            notated.push_str(line);
69            notated.push('\n');
70            notated.push_str(&repeat_str(" ", self.span().start.column - 1));
71            let note_len =
72                self.span().end.column.saturating_sub(self.span().start.column)
73                    + 1;
74            let note_len = cmp::max(1, note_len);
75            notated.push_str(&repeat_str("^", note_len));
76            notated.push('\n');
77        }
78
79        notated
80    }
81}
82
83#[cfg(test)]
84mod test {
85    use std::fmt;
86
87    use pretty_assertions::assert_eq;
88    use thiserror::Error;
89
90    use super::{repeat_str, FrontendError};
91    use crate::span::{Position, Span};
92
93    #[derive(Error, Clone, Debug, Eq, PartialEq)]
94    struct Error {
95        #[source]
96        kind: ErrorKind,
97        text: String,
98        span: Span,
99    }
100
101    #[derive(Error, Clone, Debug, Eq, PartialEq)]
102    #[non_exhaustive]
103    enum ErrorKind {
104        #[error("unexpected token '{0}'")]
105        TokenUnexpected(String),
106    }
107
108    impl FrontendError<ErrorKind> for Error {
109        fn kind(&self) -> &ErrorKind {
110            &self.kind
111        }
112
113        fn text(&self) -> &str {
114            &self.text
115        }
116
117        fn span(&self) -> &Span {
118            &self.span
119        }
120    }
121
122    impl fmt::Display for Error {
123        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124            self.format_error(f)
125        }
126    }
127
128    #[test]
129    fn test_notate() {
130        let err = Error {
131            kind: ErrorKind::TokenUnexpected("world".to_owned()),
132            text: "hello\nworld\n".to_owned(),
133            span: Span::new(Position::new(0, 2, 1), Position::new(4, 2, 5)),
134        };
135        let notated = format!("{}", err);
136
137        let mut expected = String::from("");
138        expected.push_str(&repeat_str("•", 79));
139        expected.push('\n');
140        expected
141            .push_str(format!("bulloak error: {}\n\n", err.kind()).as_str());
142        expected.push_str("world\n");
143        expected.push_str("^^^^^\n\n");
144        expected.push_str(
145            format!(
146                "--- (line {}, column {}) ---\n",
147                err.span().start.line,
148                err.span().start.column
149            )
150            .as_str(),
151        );
152        assert_eq!(expected, notated);
153    }
154}