toml_edit/
error.rs

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