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    if input.is_empty() {
121        return (
122            input,
123            Location {
124                line: 1,
125                column: 1,
126                offset: 0,
127            },
128        );
129    }
130    let offset = err.offset().min(input.len() - 1);
131    let column_offset = err.offset() - offset;
132
133    // Find the start of the line containing the error.
134    let line_begin = input[..offset]
135        .iter()
136        .rev()
137        .position(|&b| b == b'\n')
138        .map_or(0, |pos| offset - pos);
139
140    // Use the full line containing the error as context for later printing.
141    let line_context = input[line_begin..]
142        .iter()
143        .position(|&b| b == b'\n')
144        .map_or(&input[line_begin..], |pos| {
145            &input[line_begin..line_begin + pos]
146        });
147
148    // Count the number of newlines in the input before the line containing the error to calculate
149    // the line number.
150    let line = input[..line_begin].iter().filter(|&&b| b == b'\n').count() + 1;
151
152    // The (1-indexed) column number is the offset of the remaining input into that line.
153    // This also takes multi-byte unicode characters into account.
154    let column = std::str::from_utf8(&input[line_begin..=offset])
155        .map_or_else(|_| offset - line_begin + 1, |s| s.chars().count())
156        + column_offset;
157
158    (
159        line_context,
160        Location {
161            line,
162            column,
163            offset,
164        },
165    )
166}
167
168// This is almost identical to `ContextError::to_string` but produces a slightly different format
169// which does not contain line breaks and emits "unexpected token" when there was no expectation in
170// the context.
171fn format_context_error(err: &ContextError) -> String {
172    let mut buf = String::new();
173
174    let label = err.context().find_map(|c| match c {
175        StrContext::Label(c) => Some(c),
176        _ => None,
177    });
178
179    let expected = err
180        .context()
181        .filter_map(|c| match c {
182            StrContext::Expected(c) => Some(c),
183            _ => None,
184        })
185        .collect::<Vec<_>>();
186
187    if let Some(label) = label {
188        _ = write!(buf, "invalid {label}; ");
189    }
190
191    if expected.is_empty() {
192        _ = buf.write_str("unexpected token");
193    } else {
194        _ = write!(buf, "expected ");
195
196        match expected.len() {
197            0 => {}
198            1 => {
199                _ = write!(buf, "{}", &expected[0]);
200            }
201            n => {
202                for (i, expected) in expected.iter().enumerate() {
203                    if i == n - 1 {
204                        _ = buf.write_str(" or ");
205                    } else if i > 0 {
206                        _ = buf.write_str(", ");
207                    }
208
209                    _ = write!(buf, "{expected}");
210                }
211            }
212        }
213    }
214
215    if let Some(cause) = err.cause() {
216        _ = write!(buf, "; {cause}");
217    }
218
219    buf
220}