1 | use std::error::Error as StdError; |
2 | use std::fmt::{Display, Formatter, Result}; |
3 | |
4 | /// Type representing a TOML parse error |
5 | #[derive (Debug, Clone, Eq, PartialEq, Hash)] |
6 | pub struct TomlError { |
7 | message: String, |
8 | raw: Option<String>, |
9 | keys: Vec<String>, |
10 | span: Option<std::ops::Range<usize>>, |
11 | } |
12 | |
13 | impl TomlError { |
14 | #[cfg (feature = "parse" )] |
15 | pub(crate) fn new( |
16 | error: winnow::error::ParseError< |
17 | crate::parser::prelude::Input<'_>, |
18 | winnow::error::ContextError, |
19 | >, |
20 | mut raw: crate::parser::prelude::Input<'_>, |
21 | ) -> Self { |
22 | use winnow::stream::Stream; |
23 | |
24 | let message = error.inner().to_string(); |
25 | let raw = raw.finish(); |
26 | let raw = String::from_utf8(raw.to_owned()).expect("original document was utf8" ); |
27 | |
28 | let offset = error.offset(); |
29 | let offset = (0..=offset) |
30 | .rev() |
31 | .find(|index| raw.is_char_boundary(*index)) |
32 | .unwrap_or(0); |
33 | |
34 | let mut indices = raw[offset..].char_indices(); |
35 | indices.next(); |
36 | let len = if let Some((index, _)) = indices.next() { |
37 | index |
38 | } else { |
39 | raw.len() - offset |
40 | }; |
41 | let span = offset..(offset + len); |
42 | |
43 | Self { |
44 | message, |
45 | raw: Some(raw), |
46 | keys: Vec::new(), |
47 | span: Some(span), |
48 | } |
49 | } |
50 | |
51 | #[cfg (any(feature = "serde" , feature = "parse" ))] |
52 | pub(crate) fn custom(message: String, span: Option<std::ops::Range<usize>>) -> Self { |
53 | Self { |
54 | message, |
55 | raw: None, |
56 | keys: Vec::new(), |
57 | span, |
58 | } |
59 | } |
60 | |
61 | #[cfg (feature = "serde" )] |
62 | pub(crate) fn add_key(&mut self, key: String) { |
63 | self.keys.insert(0, key); |
64 | } |
65 | |
66 | /// What went wrong |
67 | pub fn message(&self) -> &str { |
68 | &self.message |
69 | } |
70 | |
71 | /// The start/end index into the original document where the error occurred |
72 | pub fn span(&self) -> Option<std::ops::Range<usize>> { |
73 | self.span.clone() |
74 | } |
75 | |
76 | #[cfg (feature = "serde" )] |
77 | pub(crate) fn set_span(&mut self, span: Option<std::ops::Range<usize>>) { |
78 | self.span = span; |
79 | } |
80 | |
81 | #[cfg (feature = "serde" )] |
82 | pub(crate) fn set_raw(&mut self, raw: Option<String>) { |
83 | self.raw = raw; |
84 | } |
85 | } |
86 | |
87 | /// Displays a TOML parse error |
88 | /// |
89 | /// # Example |
90 | /// |
91 | /// TOML parse error at line 1, column 10 |
92 | /// | |
93 | /// 1 | 00:32:00.a999999 |
94 | /// | ^ |
95 | /// Unexpected `a` |
96 | /// Expected `digit` |
97 | /// While parsing a Time |
98 | /// While parsing a Date-Time |
99 | impl Display for TomlError { |
100 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { |
101 | let mut context = false; |
102 | if let (Some(raw), Some(span)) = (&self.raw, self.span()) { |
103 | context = true; |
104 | |
105 | let (line, column) = translate_position(raw.as_bytes(), span.start); |
106 | let line_num = line + 1; |
107 | let col_num = column + 1; |
108 | let gutter = line_num.to_string().len(); |
109 | let content = raw.split(' \n' ).nth(line).expect("valid line number" ); |
110 | let highlight_len = span.end - span.start; |
111 | // Allow highlight to go one past the line |
112 | let highlight_len = highlight_len.min(content.len().saturating_sub(column)); |
113 | |
114 | writeln!(f, "TOML parse error at line {line_num}, column {col_num}" )?; |
115 | // | |
116 | for _ in 0..=gutter { |
117 | write!(f, " " )?; |
118 | } |
119 | writeln!(f, "|" )?; |
120 | |
121 | // 1 | 00:32:00.a999999 |
122 | write!(f, " {line_num} | " )?; |
123 | writeln!(f, " {content}" )?; |
124 | |
125 | // | ^ |
126 | for _ in 0..=gutter { |
127 | write!(f, " " )?; |
128 | } |
129 | write!(f, "|" )?; |
130 | for _ in 0..=column { |
131 | write!(f, " " )?; |
132 | } |
133 | // The span will be empty at eof, so we need to make sure we always print at least |
134 | // one `^` |
135 | write!(f, "^" )?; |
136 | for _ in 1..highlight_len { |
137 | write!(f, "^" )?; |
138 | } |
139 | writeln!(f)?; |
140 | } |
141 | writeln!(f, " {}" , self.message)?; |
142 | if !context && !self.keys.is_empty() { |
143 | writeln!(f, "in ` {}`" , self.keys.join("." ))?; |
144 | } |
145 | |
146 | Ok(()) |
147 | } |
148 | } |
149 | |
150 | impl StdError for TomlError { |
151 | fn description(&self) -> &'static str { |
152 | "TOML parse error" |
153 | } |
154 | } |
155 | |
156 | fn translate_position(input: &[u8], index: usize) -> (usize, usize) { |
157 | if input.is_empty() { |
158 | return (0, index); |
159 | } |
160 | |
161 | let safe_index = index.min(input.len() - 1); |
162 | let column_offset = index - safe_index; |
163 | let index = safe_index; |
164 | |
165 | let nl = input[0..index] |
166 | .iter() |
167 | .rev() |
168 | .enumerate() |
169 | .find(|(_, b)| **b == b' \n' ) |
170 | .map(|(nl, _)| index - nl - 1); |
171 | let line_start = match nl { |
172 | Some(nl) => nl + 1, |
173 | None => 0, |
174 | }; |
175 | let line = input[0..line_start].iter().filter(|b| **b == b' \n' ).count(); |
176 | |
177 | let column = std::str::from_utf8(&input[line_start..=index]) |
178 | .map(|s| s.chars().count() - 1) |
179 | .unwrap_or_else(|_| index - line_start); |
180 | let column = column + column_offset; |
181 | |
182 | (line, column) |
183 | } |
184 | |
185 | #[cfg (test)] |
186 | mod test_translate_position { |
187 | use super::*; |
188 | |
189 | #[test ] |
190 | fn empty() { |
191 | let input = b"" ; |
192 | let index = 0; |
193 | let position = translate_position(&input[..], index); |
194 | assert_eq!(position, (0, 0)); |
195 | } |
196 | |
197 | #[test ] |
198 | fn start() { |
199 | let input = b"Hello" ; |
200 | let index = 0; |
201 | let position = translate_position(&input[..], index); |
202 | assert_eq!(position, (0, 0)); |
203 | } |
204 | |
205 | #[test ] |
206 | fn end() { |
207 | let input = b"Hello" ; |
208 | let index = input.len() - 1; |
209 | let position = translate_position(&input[..], index); |
210 | assert_eq!(position, (0, input.len() - 1)); |
211 | } |
212 | |
213 | #[test ] |
214 | fn after() { |
215 | let input = b"Hello" ; |
216 | let index = input.len(); |
217 | let position = translate_position(&input[..], index); |
218 | assert_eq!(position, (0, input.len())); |
219 | } |
220 | |
221 | #[test ] |
222 | fn first_line() { |
223 | let input = b"Hello \nWorld \n" ; |
224 | let index = 2; |
225 | let position = translate_position(&input[..], index); |
226 | assert_eq!(position, (0, 2)); |
227 | } |
228 | |
229 | #[test ] |
230 | fn end_of_line() { |
231 | let input = b"Hello \nWorld \n" ; |
232 | let index = 5; |
233 | let position = translate_position(&input[..], index); |
234 | assert_eq!(position, (0, 5)); |
235 | } |
236 | |
237 | #[test ] |
238 | fn start_of_second_line() { |
239 | let input = b"Hello \nWorld \n" ; |
240 | let index = 6; |
241 | let position = translate_position(&input[..], index); |
242 | assert_eq!(position, (1, 0)); |
243 | } |
244 | |
245 | #[test ] |
246 | fn second_line() { |
247 | let input = b"Hello \nWorld \n" ; |
248 | let index = 8; |
249 | let position = translate_position(&input[..], index); |
250 | assert_eq!(position, (1, 2)); |
251 | } |
252 | } |
253 | |