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