prql_parser/
error.rs

1use std::fmt::Display;
2
3use chumsky::{error::SimpleReason, Span as ChumskySpan};
4use prql_ast::Span;
5
6use crate::{lexer::Token, PError};
7
8#[derive(Debug)]
9pub struct Error {
10    pub span: Span,
11    pub kind: ErrorKind,
12}
13
14#[derive(Debug)]
15pub enum ErrorKind {
16    Lexer(LexerError),
17    Parser(ParserError),
18}
19
20impl Display for ErrorKind {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            ErrorKind::Lexer(err) => write!(f, "{err}"),
24            ErrorKind::Parser(err) => write!(f, "{err}"),
25        }
26    }
27}
28
29#[derive(Debug)]
30pub struct LexerError(String);
31
32#[derive(Debug)]
33pub struct ParserError(String);
34
35impl Display for ParserError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", self.0)
38    }
39}
40
41impl Display for LexerError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "unexpected {}", self.0)
44    }
45}
46
47pub(crate) fn convert_lexer_error(
48    source: &str,
49    e: chumsky::error::Cheap<char>,
50    source_id: u16,
51) -> Error {
52    // TODO: is there a neater way of taking a span? We want to take it based on
53    // the chars, not the bytes, so can't just index into the str.
54    let found = source
55        .chars()
56        .skip(e.span().start)
57        .take(e.span().end() - e.span().start)
58        .collect();
59    let span = Span {
60        start: e.span().start,
61        end: e.span().end,
62        source_id,
63    };
64
65    Error {
66        span,
67        kind: ErrorKind::Lexer(LexerError(found)),
68    }
69}
70
71pub(crate) fn convert_parser_error(e: PError) -> Error {
72    let mut span = e.span();
73
74    if e.found().is_none() {
75        // found end of file
76        // fix for span outside of source
77        if span.start > 0 && span.end > 0 {
78            span.start -= 1;
79            span.end -= 1;
80        }
81    }
82
83    if let SimpleReason::Custom(message) = e.reason() {
84        return Error {
85            span: *span,
86            kind: ErrorKind::Parser(ParserError(message.clone())),
87        };
88    }
89
90    fn token_to_string(t: Option<Token>) -> String {
91        t.map(|t| DisplayToken(&t).to_string())
92            .unwrap_or_else(|| "end of input".to_string())
93    }
94
95    let is_all_whitespace = e
96        .expected()
97        .all(|t| matches!(t, None | Some(Token::NewLine)));
98    let expected: Vec<String> = e
99        .expected()
100        // TODO: could we collapse this into a `filter_map`? (though semantically
101        // identical)
102        //
103        // Only include whitespace if we're _only_ expecting whitespace
104        .filter(|t| is_all_whitespace || !matches!(t, None | Some(Token::NewLine)))
105        .cloned()
106        .map(token_to_string)
107        .collect();
108
109    let while_parsing = e
110        .label()
111        .map(|l| format!(" while parsing {l}"))
112        .unwrap_or_default();
113
114    if expected.is_empty() || expected.len() > 10 {
115        let label = token_to_string(e.found().cloned());
116
117        return Error {
118            span: *span,
119            kind: ErrorKind::Parser(ParserError(format!("unexpected {label}{while_parsing}"))),
120        };
121    }
122
123    let mut expected = expected;
124    expected.sort();
125
126    let expected = match expected.len() {
127        1 => expected.remove(0),
128        2 => expected.join(" or "),
129        _ => {
130            let last = expected.pop().unwrap();
131            format!("one of {} or {last}", expected.join(", "))
132        }
133    };
134
135    Error {
136        span: *span,
137        kind: ErrorKind::Parser(ParserError(match e.found() {
138            Some(found) => format!(
139                "{who}expected {expected}, but found {found}",
140                who = e.label().map(|l| format!("{l} ")).unwrap_or_default(),
141                found = DisplayToken(found)
142            ),
143
144            // We want a friendlier message than "found end of input"...
145            None => format!("Expected {expected}, but didn't find anything before the end."),
146        })),
147    }
148}
149
150struct DisplayToken<'a>(&'a Token);
151
152impl std::fmt::Display for DisplayToken<'_> {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self.0 {
155            Token::NewLine => write!(f, "new line"),
156            Token::Ident(arg0) => {
157                if arg0.is_empty() {
158                    write!(f, "an identifier")
159                } else {
160                    write!(f, "`{arg0}`")
161                }
162            }
163            Token::Keyword(arg0) => write!(f, "keyword {arg0}"),
164            Token::Literal(..) => write!(f, "literal"),
165            Token::Control(arg0) => write!(f, "{arg0}"),
166
167            Token::ArrowThin => f.write_str("->"),
168            Token::ArrowFat => f.write_str("=>"),
169            Token::Eq => f.write_str("=="),
170            Token::Ne => f.write_str("!="),
171            Token::Gte => f.write_str(">="),
172            Token::Lte => f.write_str("<="),
173            Token::RegexSearch => f.write_str("~="),
174            Token::And => f.write_str("&&"),
175            Token::Or => f.write_str("||"),
176            Token::Coalesce => f.write_str("??"),
177            Token::DivInt => f.write_str("//"),
178            Token::Annotate => f.write_str("@{"),
179
180            Token::Param(id) => write!(f, "${id}"),
181
182            Token::Range {
183                bind_left,
184                bind_right,
185            } => write!(
186                f,
187                "'{}..{}'",
188                if *bind_left { "" } else { " " },
189                if *bind_right { "" } else { " " }
190            ),
191            Token::Interpolation(c, s) => {
192                write!(f, "{c}\"{}\"", s)
193            }
194        }
195    }
196}