surrealdb_core/syn/error/
render.rs

1//! Module for rendering errors onto source code.
2
3use 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	/// Offset the snippet locations within the rendered error by a given number of lines and
16	/// columns.
17	///
18	/// The column offset is only applied to the any snippet which is at line 1
19	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/// Whether the snippet was truncated.
51#[derive(Clone, Copy, Eq, PartialEq, Debug)]
52pub enum Truncation {
53	/// The snippet wasn't truncated
54	None,
55	/// The snippet was truncated at the start
56	Start,
57	/// The snippet was truncated at the end
58	End,
59	/// Both sided of the snippet where truncated.
60	Both,
61}
62
63/// A piece of the source code with a location and an optional explanation.
64#[derive(Clone, Debug)]
65pub struct Snippet {
66	/// The part of the original source code,
67	source: String,
68	/// Whether part of the source line was truncated.
69	truncation: Truncation,
70	/// The location of the snippet in the original source code.
71	location: Location,
72	/// The offset, in chars, into the snippet where the location is.
73	offset: usize,
74	/// The amount of characters that are part of area to be pointed to.
75	length: usize,
76	/// A possible explanation for this snippet.
77	label: Option<String>,
78	/// The kind of snippet,
79	// Unused for now but could in the future be used to color snippets.
80	#[allow(dead_code)]
81	kind: MessageKind,
82}
83
84impl Snippet {
85	/// How long with the source line have to be before it gets truncated.
86	const MAX_SOURCE_DISPLAY_LEN: usize = 80;
87	/// How far the will have to be in the source line before everything before it gets truncated.
88	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	/// Trims whitespace of an line and additionally truncates the string around the target_col_offset if it is too long.
135	///
136	/// returns the trimmed string, how it is truncated, and the offset into truncated the string where the target_col is located.
137	fn truncate_line(mut line: &str, target_col: usize) -> (&str, Truncation, usize) {
138		// offset in characters from the start of the string.
139		let mut offset = 0;
140		for (i, (idx, c)) in line.char_indices().enumerate() {
141			// if i == target_col the error is in the leading whitespace. so return early.
142			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		// truncation none because only truncated non-whitespace counts.
151		let mut truncation = Truncation::None;
152
153		if offset > Self::MAX_ERROR_LINE_OFFSET {
154			// Actual error is to far to the right, just truncated everything to the left.
155			// show some prefix for some extra context.
156			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			// Line is too long, truncate to source
168			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			// Unwrap because we just checked if the line length is longer then this.
177			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		// extra spacing for the line number
188		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}