toml_edit/
error.rs

1use std::error::Error as StdError;
2use std::fmt::{Display, Formatter, Result};
3
4/// Type representing a TOML parse error
5#[derive(Debug, Clone, Eq, PartialEq, Hash)]
6pub struct TomlError {
7    message: String,
8    raw: Option<String>,
9    keys: Vec<String>,
10    span: Option<std::ops::Range<usize>>,
11}
12
13impl 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
99impl 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
150impl StdError for TomlError {
151    fn description(&self) -> &'static str {
152        "TOML parse error"
153    }
154}
155
156fn 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)]
186mod 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}