hcl_edit/parser/
error.rs

1use super::prelude::*;
2
3use std::fmt::{self, Write};
4use winnow::error::ParseError;
5
6/// Error type returned when the parser encountered an error.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Error {
9    inner: Box<ErrorInner>,
10}
11
12impl Error {
13    pub(super) fn from_parse_error(err: &ParseError<Input, ContextError>) -> Error {
14        Error::new(ErrorInner::from_parse_error(err))
15    }
16
17    fn new(inner: ErrorInner) -> Error {
18        Error {
19            inner: Box::new(inner),
20        }
21    }
22
23    /// Returns the message in the input where the error occurred.
24    pub fn message(&self) -> &str {
25        &self.inner.message
26    }
27
28    /// Returns the line from the input where the error occurred.
29    ///
30    /// Note that this returns the full line containing the invalid input. Use
31    /// [`.location()`][Error::location] to obtain the column in which the error starts.
32    pub fn line(&self) -> &str {
33        &self.inner.line
34    }
35
36    /// Returns the location in the input at which the error occurred.
37    pub fn location(&self) -> &Location {
38        &self.inner.location
39    }
40}
41
42impl std::error::Error for Error {}
43
44impl fmt::Display for Error {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        fmt::Display::fmt(&self.inner, f)
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51struct ErrorInner {
52    message: String,
53    line: String,
54    location: Location,
55}
56
57impl ErrorInner {
58    fn from_parse_error(err: &ParseError<Input, ContextError>) -> ErrorInner {
59        let (line, location) = locate_error(err);
60
61        ErrorInner {
62            message: format_context_error(err.inner()),
63            line: String::from_utf8_lossy(line).to_string(),
64            location,
65        }
66    }
67
68    fn spacing(&self) -> String {
69        " ".repeat(self.location.line.to_string().len())
70    }
71}
72
73impl fmt::Display for ErrorInner {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(
76            f,
77            "{s}--> HCL parse error in line {l}, column {c}\n\
78                 {s} |\n\
79                 {l} | {line}\n\
80                 {s} | {caret:>c$}---\n\
81                 {s} |\n\
82                 {s} = {message}",
83            s = self.spacing(),
84            l = self.location.line,
85            c = self.location.column,
86            line = self.line,
87            caret = '^',
88            message = self.message,
89        )
90    }
91}
92
93/// Represents a location in the parser input.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Location {
96    line: usize,
97    column: usize,
98    offset: usize,
99}
100
101impl Location {
102    /// Returns the line number (one-based) in the parser input.
103    pub fn line(&self) -> usize {
104        self.line
105    }
106
107    /// Returns the column number (one-based) in the parser input.
108    pub fn column(&self) -> usize {
109        self.column
110    }
111
112    /// Returns the byte offset (zero-based) in the parser input.
113    pub fn offset(&self) -> usize {
114        self.offset
115    }
116}
117
118fn locate_error<'a>(err: &'a ParseError<Input<'a>, ContextError>) -> (&'a [u8], Location) {
119    let input = err.input().as_bytes();
120    let offset = err.offset().min(input.len() - 1);
121    let column_offset = err.offset() - offset;
122
123    // Find the start of the line containing the error.
124    let line_begin = input[..offset]
125        .iter()
126        .rev()
127        .position(|&b| b == b'\n')
128        .map_or(0, |pos| offset - pos);
129
130    // Use the full line containing the error as context for later printing.
131    let line_context = input[line_begin..]
132        .iter()
133        .position(|&b| b == b'\n')
134        .map_or(&input[line_begin..], |pos| {
135            &input[line_begin..line_begin + pos]
136        });
137
138    // Count the number of newlines in the input before the line containing the error to calculate
139    // the line number.
140    let line = input[..line_begin].iter().filter(|&&b| b == b'\n').count() + 1;
141
142    // The (1-indexed) column number is the offset of the remaining input into that line.
143    // This also takes multi-byte unicode characters into account.
144    let column = std::str::from_utf8(&input[line_begin..=offset])
145        .map_or_else(|_| offset - line_begin + 1, |s| s.chars().count())
146        + column_offset;
147
148    (
149        line_context,
150        Location {
151            line,
152            column,
153            offset,
154        },
155    )
156}
157
158// This is almost identical to `ContextError::to_string` but produces a slightly different format
159// which does not contain line breaks and emits "unexpected token" when there was no expectation in
160// the context.
161fn format_context_error(err: &ContextError) -> String {
162    let mut buf = String::new();
163
164    let label = err.context().find_map(|c| match c {
165        StrContext::Label(c) => Some(c),
166        _ => None,
167    });
168
169    let expected = err
170        .context()
171        .filter_map(|c| match c {
172            StrContext::Expected(c) => Some(c),
173            _ => None,
174        })
175        .collect::<Vec<_>>();
176
177    if let Some(label) = label {
178        _ = write!(buf, "invalid {label}; ");
179    }
180
181    if expected.is_empty() {
182        _ = buf.write_str("unexpected token");
183    } else {
184        _ = write!(buf, "expected ");
185
186        match expected.len() {
187            0 => {}
188            1 => {
189                _ = write!(buf, "{}", &expected[0]);
190            }
191            n => {
192                for (i, expected) in expected.iter().enumerate() {
193                    if i == n - 1 {
194                        _ = buf.write_str(" or ");
195                    } else if i > 0 {
196                        _ = buf.write_str(", ");
197                    }
198
199                    _ = write!(buf, "{expected}");
200                }
201            }
202        };
203    }
204
205    if let Some(cause) = err.cause() {
206        _ = write!(buf, "; {cause}");
207    }
208
209    buf
210}