toml_edit/
error.rs

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