Skip to main content

linkrs_query/parser/core/
error.rs

1//! Error handling for the query parser
2//!
3//! This module defines error types for the parsing process,
4//! providing unified error reporting with position information,
5//! hints, and context support.
6
7use crate::QueryError;
8use std::error::Error;
9use std::fmt;
10
11use linkrs_core::types::Position;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ParseErrorKind {
15    LexicalError,
16    SyntaxError,
17    UnexpectedToken,
18    UnterminatedString,
19    UnterminatedComment,
20    InvalidNumber,
21    InvalidEscapeSequence,
22    UnicodeEscapeError,
23    UnexpectedEndOfInput,
24    InvalidCharacter,
25    UnknownKeyword,
26    RecursionLimitExceeded,
27    UnsupportedFeature,
28    SemanticError,
29}
30
31#[derive(Debug)]
32pub struct ParseError {
33    pub kind: ParseErrorKind,
34    pub message: Box<str>,
35    pub position: Position,
36    pub offset: Option<usize>,
37    pub unexpected_token: Option<Box<str>>,
38    pub expected_tokens: Box<[String]>,
39    pub context: Option<Box<dyn Error + Send + Sync>>,
40    pub hints: Box<[String]>,
41    /// Set when the error was recorded through error recovery: parsing
42    /// synchronized to the next clause/statement boundary and continued.
43    pub recovered: bool,
44}
45
46impl ParseError {
47    pub fn new(kind: ParseErrorKind, message: impl Into<Box<str>>, position: Position) -> Self {
48        ParseError {
49            kind,
50            message: message.into(),
51            position,
52            offset: None,
53            unexpected_token: None,
54            expected_tokens: Box::new([]),
55            context: None,
56            hints: Box::new([]),
57            recovered: false,
58        }
59    }
60
61    pub fn new_simple(message: impl Into<Box<str>>, position: Position) -> Self {
62        ParseError::new(ParseErrorKind::SyntaxError, message, position)
63    }
64
65    pub fn syntax_error<T: fmt::Display>(msg: T, position: Position) -> ParseError {
66        ParseError::new(
67            ParseErrorKind::SyntaxError,
68            format!("Syntax error: {}", msg),
69            position,
70        )
71    }
72
73    pub fn unexpected_token<T: fmt::Display>(token: T, position: Position) -> ParseError {
74        ParseError::new(
75            ParseErrorKind::UnexpectedToken,
76            format!("Unexpected token: {}", token),
77            position,
78        )
79    }
80
81    pub fn unterminated_string(position: Position) -> ParseError {
82        ParseError::new(
83            ParseErrorKind::UnterminatedString,
84            "Unterminated string literal",
85            position,
86        )
87    }
88
89    pub fn unterminated_comment(position: Position) -> ParseError {
90        ParseError::new(
91            ParseErrorKind::UnterminatedComment,
92            "Unterminated multi-line comment",
93            position,
94        )
95    }
96
97    pub fn with_offset(mut self, offset: usize) -> Self {
98        self.offset = Some(offset);
99        self
100    }
101
102    pub fn with_unexpected_token<T: fmt::Display>(mut self, token: T) -> Self {
103        self.unexpected_token = Some(token.to_string().into_boxed_str());
104        self
105    }
106
107    pub fn with_expected_tokens(mut self, tokens: Vec<String>) -> Self {
108        self.expected_tokens = tokens.into_boxed_slice();
109        self
110    }
111
112    pub fn with_context<E: Error + Send + Sync + 'static>(mut self, context: E) -> Self {
113        self.context = Some(Box::new(context));
114        self
115    }
116
117    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
118        self.hints = std::iter::once(hint.into())
119            .collect::<Vec<String>>()
120            .into_boxed_slice();
121        self
122    }
123
124    pub fn with_hints(mut self, hints: Vec<impl Into<String>>) -> Self {
125        self.hints = hints
126            .into_iter()
127            .map(|h| h.into())
128            .collect::<Vec<String>>()
129            .into_boxed_slice();
130        self
131    }
132
133    pub fn add_hint(&mut self, hint: impl Into<String>) {
134        let mut new_hints = self
135            .hints
136            .iter()
137            .map(|s| s.to_string())
138            .collect::<Vec<String>>();
139        new_hints.push(hint.into());
140        self.hints = new_hints.into_boxed_slice();
141    }
142
143    /// Mark the error as produced during error recovery (parsing continued
144    /// after synchronizing to the next clause boundary).
145    pub fn mark_recovered(mut self) -> Self {
146        self.recovered = true;
147        self
148    }
149}
150
151impl fmt::Display for ParseError {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(
154            f,
155            "Parse error at line {}, column {}: {}",
156            self.position.line, self.position.column, self.message
157        )?;
158
159        if let Some(ref token) = self.unexpected_token {
160            writeln!(f, "\n  Unexpected token: {}", token)?;
161        }
162
163        if !self.expected_tokens.is_empty() {
164            writeln!(
165                f,
166                "\n  Expected one of: {}",
167                self.expected_tokens.join(", ")
168            )?;
169        }
170
171        if let Some(ref context) = self.context {
172            writeln!(f, "\n  Context: {}", context)?;
173        }
174
175        if !self.hints.is_empty() {
176            writeln!(f, "\n  Hint(s):")?;
177            for hint in &self.hints {
178                writeln!(f, "    - {}", hint)?;
179            }
180        }
181
182        if self.recovered {
183            writeln!(f, "\n  (recovered, parsing continued)")?;
184        }
185
186        Ok(())
187    }
188}
189
190impl Error for ParseError {}
191
192impl From<String> for ParseError {
193    fn from(message: String) -> Self {
194        ParseError::new(ParseErrorKind::SyntaxError, message, Position::new(0, 0))
195    }
196}
197
198impl From<super::super::lexing::LexError> for ParseError {
199    fn from(lex_error: super::super::lexing::LexError) -> Self {
200        let mut parse_error = ParseError::new(
201            ParseErrorKind::LexicalError,
202            lex_error.message,
203            lex_error.position,
204        );
205        // Retain the offset information.
206        if let Some(offset) = lex_error.offset {
207            parse_error = parse_error.with_offset(offset);
208        }
209        parse_error
210    }
211}
212
213impl From<ParseError> for QueryError {
214    fn from(parse_error: ParseError) -> Self {
215        use linkrs_core::error::query::{
216            ParseErrorKind as QueryParseErrorKind, StructuredParseError,
217        };
218        use linkrs_core::types::Position;
219
220        let kind = match parse_error.kind {
221            ParseErrorKind::LexicalError => QueryParseErrorKind::LexicalError,
222            ParseErrorKind::SyntaxError => QueryParseErrorKind::SyntaxError,
223            ParseErrorKind::UnexpectedToken => QueryParseErrorKind::UnexpectedToken,
224            ParseErrorKind::UnterminatedString => QueryParseErrorKind::UnterminatedString,
225            ParseErrorKind::UnterminatedComment => QueryParseErrorKind::UnterminatedComment,
226            ParseErrorKind::InvalidNumber => QueryParseErrorKind::InvalidNumber,
227            ParseErrorKind::InvalidEscapeSequence => QueryParseErrorKind::InvalidEscapeSequence,
228            ParseErrorKind::UnicodeEscapeError => QueryParseErrorKind::UnicodeEscapeError,
229            ParseErrorKind::UnexpectedEndOfInput => QueryParseErrorKind::UnexpectedEndOfInput,
230            ParseErrorKind::InvalidCharacter => QueryParseErrorKind::InvalidCharacter,
231            ParseErrorKind::UnknownKeyword => QueryParseErrorKind::UnknownKeyword,
232            ParseErrorKind::RecursionLimitExceeded => QueryParseErrorKind::RecursionLimitExceeded,
233            ParseErrorKind::UnsupportedFeature => QueryParseErrorKind::UnsupportedFeature,
234            ParseErrorKind::SemanticError => QueryParseErrorKind::SemanticError,
235        };
236
237        let structured = StructuredParseError {
238            kind,
239            message: parse_error.message.into(),
240            position: Position::new(parse_error.position.line, parse_error.position.column),
241            offset: parse_error.offset,
242            unexpected_token: parse_error.unexpected_token.map(|t| t.into()),
243            expected_tokens: parse_error.expected_tokens.into_vec(),
244            hints: parse_error.hints.into_vec(),
245            context: parse_error.context.as_ref().map(|c| c.to_string()),
246            recovered: parse_error.recovered,
247        };
248
249        QueryError::structured_parse_error(structured)
250    }
251}
252
253#[derive(Debug)]
254pub struct ParseErrors {
255    pub errors: Vec<ParseError>,
256}
257
258impl ParseErrors {
259    pub fn new() -> Self {
260        ParseErrors { errors: Vec::new() }
261    }
262
263    pub fn add(&mut self, error: ParseError) {
264        self.errors.push(error);
265    }
266
267    pub fn is_empty(&self) -> bool {
268        self.errors.is_empty()
269    }
270
271    pub fn len(&self) -> usize {
272        self.errors.len()
273    }
274
275    pub fn push(&mut self, error: ParseError) {
276        self.errors.push(error);
277    }
278
279    pub fn extend(&mut self, errors: &mut ParseErrors) {
280        self.errors.append(&mut errors.errors);
281    }
282
283    pub fn take(&mut self) -> Vec<ParseError> {
284        std::mem::take(&mut self.errors)
285    }
286
287    pub fn iter(&self) -> impl Iterator<Item = &ParseError> {
288        self.errors.iter()
289    }
290}
291
292impl IntoIterator for ParseErrors {
293    type Item = ParseError;
294    type IntoIter = std::vec::IntoIter<ParseError>;
295
296    fn into_iter(self) -> Self::IntoIter {
297        self.errors.into_iter()
298    }
299}
300
301impl Default for ParseErrors {
302    fn default() -> Self {
303        ParseErrors::new()
304    }
305}
306
307impl fmt::Display for ParseErrors {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        for (i, error) in self.errors.iter().enumerate() {
310            if i > 0 {
311                writeln!(f)?;
312            }
313            write!(f, "{}", error)?;
314        }
315        Ok(())
316    }
317}
318
319impl Error for ParseErrors {}
320
321impl From<Vec<ParseError>> for ParseErrors {
322    fn from(errors: Vec<ParseError>) -> Self {
323        ParseErrors { errors }
324    }
325}
326
327impl From<ParseErrors> for QueryError {
328    fn from(parse_errors: ParseErrors) -> Self {
329        QueryError::parse_error(parse_errors.to_string())
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_parse_error_display() {
339        let error = ParseError::unexpected_token("IDENTIFIER", Position::new(10, 5));
340        let display = error.to_string();
341        assert!(display.contains("line 10, column 5"));
342        assert!(display.contains("Unexpected token: IDENTIFIER"));
343    }
344
345    #[test]
346    fn test_parse_error_with_hint() {
347        let error = ParseError::syntax_error("invalid syntax", Position::new(5, 10))
348            .with_hint("Try adding a semicolon at the end".to_string());
349
350        let display = error.to_string();
351        assert!(display.contains("Hint"));
352        assert!(display.contains("semicolon"));
353    }
354
355    #[test]
356    fn test_parse_error_with_context() {
357        let context_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
358        let error =
359            ParseError::syntax_error("error", Position::new(1, 1)).with_context(context_error);
360
361        let display = error.to_string();
362        assert!(display.contains("Context"));
363    }
364}