1use std::{cmp, fmt};
2
3use crate::{span::Span, utils::repeat_str};
4
5pub trait FrontendError<K: fmt::Display>: std::error::Error {
10 #[must_use]
12 fn kind(&self) -> &K;
13
14 #[must_use]
16 fn text(&self) -> &str;
17
18 #[must_use]
20 fn span(&self) -> &Span;
21
22 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 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}