Skip to main content

surql_parser/upstream/syn/error/
render.rs

1//! Module for rendering errors onto source code.
2use super::{Location, MessageKind};
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Range;
6#[derive(Clone, Debug)]
7pub struct RenderedError {
8	pub errors: Vec<String>,
9	pub snippets: Vec<Snippet>,
10}
11impl RenderedError {
12	/// Offset the snippet locations within the rendered error by a given number
13	/// of lines and columns.
14	///
15	/// The column offset is only applied to the any snippet which is at line 1
16	pub fn offset_location(mut self, line: usize, col: usize) -> Self {
17		for s in self.snippets.iter_mut() {
18			if s.location.line == 1 {
19				s.location.column += col;
20			}
21			s.location.line += line;
22		}
23		self
24	}
25}
26impl fmt::Display for RenderedError {
27	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28		match self.errors.len().cmp(&1) {
29			Ordering::Equal => writeln!(f, "{}", self.errors[0])?,
30			Ordering::Greater => {
31				writeln!(f, "- {}", self.errors[0])?;
32				writeln!(f, "caused by:")?;
33				for e in &self.errors[2..] {
34					writeln!(f, "    - {}", e)?
35				}
36			}
37			Ordering::Less => {}
38		}
39		for s in &self.snippets {
40			writeln!(f, "{s}")?;
41		}
42		Ok(())
43	}
44}
45/// Whether the snippet was truncated.
46#[derive(Clone, Copy, Eq, PartialEq, Debug)]
47pub enum Truncation {
48	/// The snippet wasn't truncated
49	None,
50	/// The snippet was truncated at the start
51	Start,
52	/// The snippet was truncated at the end
53	End,
54	/// Both sided of the snippet where truncated.
55	Both,
56}
57/// A piece of the source code with a location and an optional explanation.
58#[derive(Clone, Debug)]
59pub struct Snippet {
60	/// The part of the original source code,
61	source: String,
62	/// Whether part of the source line was truncated.
63	truncation: Truncation,
64	/// The location of the snippet in the original source code.
65	location: Location,
66	/// The offset, in chars, into the snippet where the location is.
67	offset: usize,
68	/// The amount of characters that are part of area to be pointed to.
69	length: usize,
70	/// A possible explanation for this snippet.
71	label: Option<String>,
72	/// The kind of snippet,
73	#[expect(dead_code)]
74	kind: MessageKind,
75}
76impl Snippet {
77	/// How long with the source line have to be before it gets truncated.
78	const MAX_SOURCE_DISPLAY_LEN: usize = 80;
79	/// How far the will have to be in the source line before everything before
80	/// it gets truncated.
81	const MAX_ERROR_LINE_OFFSET: usize = 50;
82	pub fn from_source_location(
83		source: &str,
84		location: Location,
85		explain: Option<&'static str>,
86		kind: MessageKind,
87	) -> Self {
88		let line = source
89			.split('\n')
90			.nth(location.line - 1)
91			.expect("line exists in source");
92		let (line, truncation, offset) = Self::truncate_line(line, location.column - 1);
93		Snippet {
94			source: line.to_owned(),
95			truncation,
96			location,
97			offset,
98			length: 1,
99			label: explain.map(|x| x.into()),
100			kind,
101		}
102	}
103	pub fn from_source_location_range(
104		source: &str,
105		location: Range<Location>,
106		explain: Option<&str>,
107		kind: MessageKind,
108	) -> Self {
109		let line = source
110			.split('\n')
111			.nth(location.start.line - 1)
112			.expect("line exists in source");
113		let (line, truncation, offset) = Self::truncate_line(line, location.start.column - 1);
114		let length = if location.start.line == location.end.line {
115			(location.end.column - location.start.column).max(1)
116		} else {
117			1
118		};
119		Snippet {
120			source: line.to_owned(),
121			truncation,
122			location: location.start,
123			offset,
124			length,
125			label: explain.map(|x| x.into()),
126			kind,
127		}
128	}
129	/// Trims whitespace of an line and additionally truncates the string around
130	/// the target_col_offset if it is too long.
131	///
132	/// returns the trimmed string, how it is truncated, and the offset into
133	/// truncated the string where the target_col is located.
134	fn truncate_line(mut line: &str, target_col: usize) -> (&str, Truncation, usize) {
135		let mut offset = 0;
136		for (i, (idx, c)) in line.char_indices().enumerate() {
137			if i == target_col || !c.is_whitespace() {
138				line = &line[idx..];
139				offset = target_col - i;
140				break;
141			}
142		}
143		line = line.trim_end();
144		let mut truncation = Truncation::None;
145		if offset > Self::MAX_ERROR_LINE_OFFSET {
146			let too_much_offset = offset - 10;
147			let mut chars = line.chars();
148			for _ in 0..too_much_offset {
149				chars.next();
150			}
151			offset = 10;
152			line = chars.as_str();
153			truncation = Truncation::Start;
154		}
155		if line.chars().count() > Self::MAX_SOURCE_DISPLAY_LEN {
156			let mut size = Self::MAX_SOURCE_DISPLAY_LEN - 3;
157			if truncation == Truncation::Start {
158				truncation = Truncation::Both;
159				size -= 3;
160			} else {
161				truncation = Truncation::End
162			}
163			let truncate_index = line
164				.char_indices()
165				.nth(size)
166				.expect("character index exists")
167				.0;
168			line = &line[..truncate_index];
169		}
170		(line, truncation, offset)
171	}
172}
173impl fmt::Display for Snippet {
174	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175		let spacing = self.location.line.ilog10() as usize + 1;
176		for _ in 0..spacing {
177			f.write_str(" ")?;
178		}
179		writeln!(f, "--> [{}:{}]", self.location.line, self.location.column)?;
180		for _ in 0..spacing {
181			f.write_str(" ")?;
182		}
183		f.write_str(" |\n")?;
184		write!(f, "{:>spacing$} | ", self.location.line)?;
185		match self.truncation {
186			Truncation::None => {
187				writeln!(f, "{}", self.source)?;
188			}
189			Truncation::Start => {
190				writeln!(f, "...{}", self.source)?;
191			}
192			Truncation::End => {
193				writeln!(f, "{}...", self.source)?;
194			}
195			Truncation::Both => {
196				writeln!(f, "...{}...", self.source)?;
197			}
198		}
199		let error_offset = self.offset
200			+ if matches!(self.truncation, Truncation::Start | Truncation::Both) {
201				3
202			} else {
203				0
204			};
205		for _ in 0..spacing {
206			f.write_str(" ")?;
207		}
208		f.write_str(" | ")?;
209		for _ in 0..error_offset {
210			f.write_str(" ")?;
211		}
212		for _ in 0..self.length {
213			write!(f, "^")?;
214		}
215		if let Some(ref explain) = self.label {
216			write!(f, " {explain}")?;
217		}
218		Ok(())
219	}
220}