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