hcl_edit/parser/
error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
use super::prelude::*;

use std::fmt::{self, Write};
use winnow::error::ParseError;

/// Error type returned when the parser encountered an error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
    inner: Box<ErrorInner>,
}

impl Error {
    pub(super) fn from_parse_error(err: &ParseError<Input, ContextError>) -> Error {
        Error::new(ErrorInner::from_parse_error(err))
    }

    fn new(inner: ErrorInner) -> Error {
        Error {
            inner: Box::new(inner),
        }
    }

    /// Returns the message in the input where the error occurred.
    pub fn message(&self) -> &str {
        &self.inner.message
    }

    /// Returns the line from the input where the error occurred.
    ///
    /// Note that this returns the full line containing the invalid input. Use
    /// [`.location()`][Error::location] to obtain the column in which the error starts.
    pub fn line(&self) -> &str {
        &self.inner.line
    }

    /// Returns the location in the input at which the error occurred.
    pub fn location(&self) -> &Location {
        &self.inner.location
    }
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.inner, f)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ErrorInner {
    message: String,
    line: String,
    location: Location,
}

impl ErrorInner {
    fn from_parse_error(err: &ParseError<Input, ContextError>) -> ErrorInner {
        let (line, location) = locate_error(err);

        ErrorInner {
            message: format_context_error(err.inner()),
            line: String::from_utf8_lossy(line).to_string(),
            location,
        }
    }

    fn spacing(&self) -> String {
        " ".repeat(self.location.line.to_string().len())
    }
}

impl fmt::Display for ErrorInner {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{s}--> HCL parse error in line {l}, column {c}\n\
                 {s} |\n\
                 {l} | {line}\n\
                 {s} | {caret:>c$}---\n\
                 {s} |\n\
                 {s} = {message}",
            s = self.spacing(),
            l = self.location.line,
            c = self.location.column,
            line = self.line,
            caret = '^',
            message = self.message,
        )
    }
}

/// Represents a location in the parser input.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
    line: usize,
    column: usize,
    offset: usize,
}

impl Location {
    /// Returns the line number (one-based) in the parser input.
    pub fn line(&self) -> usize {
        self.line
    }

    /// Returns the column number (one-based) in the parser input.
    pub fn column(&self) -> usize {
        self.column
    }

    /// Returns the byte offset (zero-based) in the parser input.
    pub fn offset(&self) -> usize {
        self.offset
    }
}

fn locate_error<'a>(err: &'a ParseError<Input<'a>, ContextError>) -> (&'a [u8], Location) {
    let input = err.input().as_bytes();
    let offset = err.offset().min(input.len() - 1);
    let column_offset = err.offset() - offset;

    // Find the start of the line containing the error.
    let line_begin = input[..offset]
        .iter()
        .rev()
        .position(|&b| b == b'\n')
        .map_or(0, |pos| offset - pos);

    // Use the full line containing the error as context for later printing.
    let line_context = input[line_begin..]
        .iter()
        .position(|&b| b == b'\n')
        .map_or(&input[line_begin..], |pos| {
            &input[line_begin..line_begin + pos]
        });

    // Count the number of newlines in the input before the line containing the error to calculate
    // the line number.
    let line = input[..line_begin].iter().filter(|&&b| b == b'\n').count() + 1;

    // The (1-indexed) column number is the offset of the remaining input into that line.
    // This also takes multi-byte unicode characters into account.
    let column = std::str::from_utf8(&input[line_begin..=offset])
        .map_or_else(|_| offset - line_begin + 1, |s| s.chars().count())
        + column_offset;

    (
        line_context,
        Location {
            line,
            column,
            offset,
        },
    )
}

// This is almost identical to `ContextError::to_string` but produces a slightly different format
// which does not contain line breaks and emits "unexpected token" when there was no expectation in
// the context.
fn format_context_error(err: &ContextError) -> String {
    let mut buf = String::new();

    let label = err.context().find_map(|c| match c {
        StrContext::Label(c) => Some(c),
        _ => None,
    });

    let expected = err
        .context()
        .filter_map(|c| match c {
            StrContext::Expected(c) => Some(c),
            _ => None,
        })
        .collect::<Vec<_>>();

    if let Some(label) = label {
        _ = write!(buf, "invalid {label}; ");
    }

    if expected.is_empty() {
        _ = buf.write_str("unexpected token");
    } else {
        _ = write!(buf, "expected ");

        match expected.len() {
            0 => {}
            1 => {
                _ = write!(buf, "{}", &expected[0]);
            }
            n => {
                for (i, expected) in expected.iter().enumerate() {
                    if i == n - 1 {
                        _ = buf.write_str(" or ");
                    } else if i > 0 {
                        _ = buf.write_str(", ");
                    }

                    _ = write!(buf, "{expected}");
                }
            }
        };
    }

    if let Some(cause) = err.cause() {
        _ = write!(buf, "; {cause}");
    }

    buf
}