surrealdb_core/syn/error/
render.rs1use std::{cmp::Ordering, fmt, ops::Range};
4
5use super::{Location, MessageKind};
6
7#[derive(Clone, Debug)]
8#[non_exhaustive]
9pub struct RenderedError {
10 pub errors: Vec<String>,
11 pub snippets: Vec<Snippet>,
12}
13
14impl RenderedError {
15 pub fn offset_location(mut self, line: usize, col: usize) -> Self {
20 for s in self.snippets.iter_mut() {
21 if s.location.line == 1 {
22 s.location.column += col;
23 }
24 s.location.line += line
25 }
26 self
27 }
28}
29
30impl fmt::Display for RenderedError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self.errors.len().cmp(&1) {
33 Ordering::Equal => writeln!(f, "{}", self.errors[0])?,
34 Ordering::Greater => {
35 writeln!(f, "- {}", self.errors[0])?;
36 writeln!(f, "caused by:")?;
37 for e in &self.errors[2..] {
38 writeln!(f, " - {}", e)?
39 }
40 }
41 Ordering::Less => {}
42 }
43 for s in &self.snippets {
44 writeln!(f, "{s}")?;
45 }
46 Ok(())
47 }
48}
49
50#[derive(Clone, Copy, Eq, PartialEq, Debug)]
52pub enum Truncation {
53 None,
55 Start,
57 End,
59 Both,
61}
62
63#[derive(Clone, Debug)]
65pub struct Snippet {
66 source: String,
68 truncation: Truncation,
70 location: Location,
72 offset: usize,
74 length: usize,
76 label: Option<String>,
78 #[allow(dead_code)]
81 kind: MessageKind,
82}
83
84impl Snippet {
85 const MAX_SOURCE_DISPLAY_LEN: usize = 80;
87 const MAX_ERROR_LINE_OFFSET: usize = 50;
89
90 pub fn from_source_location(
91 source: &str,
92 location: Location,
93 explain: Option<&'static str>,
94 kind: MessageKind,
95 ) -> Self {
96 let line = source.split('\n').nth(location.line - 1).unwrap();
97 let (line, truncation, offset) = Self::truncate_line(line, location.column - 1);
98
99 Snippet {
100 source: line.to_owned(),
101 truncation,
102 location,
103 offset,
104 length: 1,
105 label: explain.map(|x| x.into()),
106 kind,
107 }
108 }
109
110 pub fn from_source_location_range(
111 source: &str,
112 location: Range<Location>,
113 explain: Option<&str>,
114 kind: MessageKind,
115 ) -> Self {
116 let line = source.split('\n').nth(location.start.line - 1).unwrap();
117 let (line, truncation, offset) = Self::truncate_line(line, location.start.column - 1);
118 let length = if location.start.line == location.end.line {
119 location.end.column - location.start.column
120 } else {
121 1
122 };
123 Snippet {
124 source: line.to_owned(),
125 truncation,
126 location: location.start,
127 offset,
128 length,
129 label: explain.map(|x| x.into()),
130 kind,
131 }
132 }
133
134 fn truncate_line(mut line: &str, target_col: usize) -> (&str, Truncation, usize) {
138 let mut offset = 0;
140 for (i, (idx, c)) in line.char_indices().enumerate() {
141 if i == target_col || !c.is_whitespace() {
143 line = &line[idx..];
144 offset = target_col - i;
145 break;
146 }
147 }
148
149 line = line.trim_end();
150 let mut truncation = Truncation::None;
152
153 if offset > Self::MAX_ERROR_LINE_OFFSET {
154 let too_much_offset = offset - 10;
157 let mut chars = line.chars();
158 for _ in 0..too_much_offset {
159 chars.next();
160 }
161 offset = 10;
162 line = chars.as_str();
163 truncation = Truncation::Start;
164 }
165
166 if line.chars().count() > Self::MAX_SOURCE_DISPLAY_LEN {
167 let mut size = Self::MAX_SOURCE_DISPLAY_LEN - 3;
169 if truncation == Truncation::Start {
170 truncation = Truncation::Both;
171 size -= 3;
172 } else {
173 truncation = Truncation::End
174 }
175
176 let truncate_index = line.char_indices().nth(size).unwrap().0;
178 line = &line[..truncate_index];
179 }
180
181 (line, truncation, offset)
182 }
183}
184
185impl fmt::Display for Snippet {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 let spacing = self.location.line.ilog10() as usize + 1;
189 for _ in 0..spacing {
190 f.write_str(" ")?;
191 }
192 writeln!(f, "--> [{}:{}]", self.location.line, self.location.column)?;
193
194 for _ in 0..spacing {
195 f.write_str(" ")?;
196 }
197 f.write_str(" |\n")?;
198 write!(f, "{:>spacing$} | ", self.location.line)?;
199 match self.truncation {
200 Truncation::None => {
201 writeln!(f, "{}", self.source)?;
202 }
203 Truncation::Start => {
204 writeln!(f, "...{}", self.source)?;
205 }
206 Truncation::End => {
207 writeln!(f, "{}...", self.source)?;
208 }
209 Truncation::Both => {
210 writeln!(f, "...{}...", self.source)?;
211 }
212 }
213
214 let error_offset = self.offset
215 + if matches!(self.truncation, Truncation::Start | Truncation::Both) {
216 3
217 } else {
218 0
219 };
220 for _ in 0..spacing {
221 f.write_str(" ")?;
222 }
223 f.write_str(" | ")?;
224 for _ in 0..error_offset {
225 f.write_str(" ")?;
226 }
227 for _ in 0..self.length {
228 write!(f, "^")?;
229 }
230 write!(f, " ")?;
231 if let Some(ref explain) = self.label {
232 write!(f, "{explain}")?;
233 }
234 Ok(())
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use super::{RenderedError, Snippet, Truncation};
241 use crate::syn::{
242 error::{Location, MessageKind},
243 token::Span,
244 };
245
246 #[test]
247 fn truncate_whitespace() {
248 let source = "\n\n\n\t $ \t";
249 let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
250
251 let location = Location::range_of_span(
252 source,
253 Span {
254 offset: offset as u32,
255 len: 1,
256 },
257 );
258
259 let snippet =
260 Snippet::from_source_location(source, location.start, None, MessageKind::Error);
261 assert_eq!(snippet.truncation, Truncation::None);
262 assert_eq!(snippet.offset, 0);
263 assert_eq!(snippet.source.as_str(), "$");
264 }
265
266 #[test]
267 fn truncate_start() {
268 let source = " aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa $ \t";
269 let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
270
271 let location = Location::range_of_span(
272 source,
273 Span {
274 offset: offset as u32,
275 len: 1,
276 },
277 );
278
279 let snippet =
280 Snippet::from_source_location(source, location.start, None, MessageKind::Error);
281 assert_eq!(snippet.truncation, Truncation::Start);
282 assert_eq!(snippet.offset, 10);
283 assert_eq!(snippet.source.as_str(), "aaaaaaaaa $");
284 }
285
286 #[test]
287 fn truncate_end() {
288 let source = "\n\n a $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \t";
289 let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
290
291 let location = Location::range_of_span(
292 source,
293 Span {
294 offset: offset as u32,
295 len: 1,
296 },
297 );
298
299 let snippet =
300 Snippet::from_source_location(source, location.start, None, MessageKind::Error);
301 assert_eq!(snippet.truncation, Truncation::End);
302 assert_eq!(snippet.offset, 2);
303 assert_eq!(
304 snippet.source.as_str(),
305 "a $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
306 );
307 }
308
309 #[test]
310 fn truncate_both() {
311 let source = "\n\n\n\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \t";
312 let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
313
314 let location = Location::range_of_span(
315 source,
316 Span {
317 offset: offset as u32,
318 len: 1,
319 },
320 );
321
322 let snippet =
323 Snippet::from_source_location(source, location.start, None, MessageKind::Error);
324 assert_eq!(snippet.truncation, Truncation::Both);
325 assert_eq!(snippet.offset, 10);
326 assert_eq!(
327 snippet.source.as_str(),
328 "aaaaaaaaa $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
329 );
330 }
331
332 #[test]
333 fn render() {
334 let error = RenderedError {
335 errors: vec!["some_error".to_string()],
336 snippets: vec![Snippet {
337 source: "hallo error".to_owned(),
338 truncation: Truncation::Both,
339 location: Location {
340 line: 4,
341 column: 10,
342 },
343 offset: 6,
344 length: 5,
345 label: Some("this is wrong".to_owned()),
346 kind: MessageKind::Error,
347 }],
348 };
349
350 let error_string = format!("{}", error);
351 let expected = r#"some_error
352 --> [4:10]
353 |
3544 | ...hallo error...
355 | ^^^^^ this is wrong
356"#;
357 assert_eq!(error_string, expected)
358 }
359}