1use crate::lexer::LexError;
2use crate::token::Span;
3use std::fmt;
4use std::path::{Path, PathBuf};
5use unicode_width::UnicodeWidthStr;
6
7/// A convenience error type to tie together all the detailed errors produced by
8/// this crate.
9///
10/// This type can be created from a [`LexError`]. This also contains
11/// storage for file/text information so a nice error can be rendered along the
12/// same lines of rustc's own error messages (minus the color).
13///
14/// This type is typically suitable for use in public APIs for consumers of this
15/// crate.
16#[derive(Debug)]
17pub struct Error {
18 inner: Box<ErrorInner>,
19}
20
21#[derive(Debug)]
22struct ErrorInner {
23 text: Option<Text>,
24 file: Option<PathBuf>,
25 span: Span,
26 kind: ErrorKind,
27}
28
29#[derive(Debug)]
30struct Text {
31 line: usize,
32 col: usize,
33 snippet: String,
34}
35
36#[derive(Debug)]
37enum ErrorKind {
38 Lex(LexError),
39 Custom(String),
40}
41
42impl Error {
43 pub(crate) fn lex(span: Span, content: &str, kind: LexError) -> Error {
44 let mut ret = Error {
45 inner: Box::new(ErrorInner {
46 text: None,
47 file: None,
48 span,
49 kind: ErrorKind::Lex(kind),
50 }),
51 };
52 ret.set_text(content);
53 ret
54 }
55
56 pub(crate) fn parse(span: Span, content: &str, message: String) -> Error {
57 let mut ret = Error {
58 inner: Box::new(ErrorInner {
59 text: None,
60 file: None,
61 span,
62 kind: ErrorKind::Custom(message),
63 }),
64 };
65 ret.set_text(content);
66 ret
67 }
68
69 /// Creates a new error with the given `message` which is targeted at the
70 /// given `span`
71 ///
72 /// Note that you'll want to ensure that `set_text` or `set_path` is called
73 /// on the resulting error to improve the rendering of the error message.
74 pub fn new(span: Span, message: String) -> Error {
75 Error {
76 inner: Box::new(ErrorInner {
77 text: None,
78 file: None,
79 span,
80 kind: ErrorKind::Custom(message),
81 }),
82 }
83 }
84
85 /// Return the `Span` for this error.
86 pub fn span(&self) -> Span {
87 self.inner.span
88 }
89
90 /// To provide a more useful error this function can be used to extract
91 /// relevant textual information about this error into the error itself.
92 ///
93 /// The `contents` here should be the full text of the original file being
94 /// parsed, and this will extract a sub-slice as necessary to render in the
95 /// `Display` implementation later on.
96 pub fn set_text(&mut self, contents: &str) {
97 if self.inner.text.is_some() {
98 return;
99 }
100 self.inner.text = Some(Text::new(contents, self.inner.span));
101 }
102
103 /// To provide a more useful error this function can be used to set
104 /// the file name that this error is associated with.
105 ///
106 /// The `path` here will be stored in this error and later rendered in the
107 /// `Display` implementation.
108 pub fn set_path(&mut self, path: &Path) {
109 if self.inner.file.is_some() {
110 return;
111 }
112 self.inner.file = Some(path.to_path_buf());
113 }
114
115 /// Returns the underlying `LexError`, if any, that describes this error.
116 pub fn lex_error(&self) -> Option<&LexError> {
117 match &self.inner.kind {
118 ErrorKind::Lex(e) => Some(e),
119 _ => None,
120 }
121 }
122
123 /// Returns the underlying message, if any, that describes this error.
124 pub fn message(&self) -> String {
125 match &self.inner.kind {
126 ErrorKind::Lex(e) => e.to_string(),
127 ErrorKind::Custom(e) => e.clone(),
128 }
129 }
130}
131
132impl fmt::Display for Error {
133 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134 let err = match &self.inner.kind {
135 ErrorKind::Lex(e) => e as &dyn fmt::Display,
136 ErrorKind::Custom(e) => e as &dyn fmt::Display,
137 };
138 let text = match &self.inner.text {
139 Some(text) => text,
140 None => {
141 return write!(f, "{} at byte offset {}", err, self.inner.span.offset);
142 }
143 };
144 let file = self
145 .inner
146 .file
147 .as_ref()
148 .and_then(|p| p.to_str())
149 .unwrap_or("<anon>");
150 write!(
151 f,
152 "\
153{err}
154 --> {file}:{line}:{col}
155 |
156 {line:4} | {text}
157 | {marker:>0$}",
158 text.col + 1,
159 file = file,
160 line = text.line + 1,
161 col = text.col + 1,
162 err = err,
163 text = text.snippet,
164 marker = "^",
165 )
166 }
167}
168
169impl std::error::Error for Error {}
170
171impl Text {
172 fn new(content: &str, span: Span) -> Text {
173 let (line, col) = span.linecol_in(content);
174 let contents = content.lines().nth(line).unwrap_or("");
175 let mut snippet = String::new();
176 for ch in contents.chars() {
177 match ch {
178 // Replace tabs with spaces to render consistently
179 '\t' => {
180 snippet.push_str(" ");
181 }
182 // these codepoints change how text is rendered so for clarity
183 // in error messages they're dropped.
184 '\u{202a}' | '\u{202b}' | '\u{202d}' | '\u{202e}' | '\u{2066}' | '\u{2067}'
185 | '\u{2068}' | '\u{206c}' | '\u{2069}' => {}
186
187 c => snippet.push(c),
188 }
189 }
190 // Use the `unicode-width` crate to figure out how wide the snippet, up
191 // to our "column", actually is. That'll tell us how many spaces to
192 // place before the `^` character that points at the problem
193 let col = snippet.get(..col).map(|s| s.width()).unwrap_or(col);
194 Text { line, col, snippet }
195 }
196}
197