sqparse/parser/error.rs
1use crate::annotation::{display_annotations, Annotation, Mode};
2use crate::parser::context::ContextType;
3use crate::token::TerminalToken;
4use crate::TokenItem;
5use std::ops::Range;
6use yansi::Paint;
7
8/// Type of [`ParseError`].
9///
10/// Implements [`std::fmt::Display`] to write a useful error message.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum ParseErrorType {
13 /// Expected a specific terminal token but got something else.
14 ///
15 /// # Example
16 /// ```text
17 /// function MyFunc { }
18 /// ^ error
19 /// ```
20 ExpectedTerminal(TerminalToken),
21
22 /// Expected a specific compound terminal but got something else.
23 ExpectedCompound2(TerminalToken, TerminalToken),
24
25 /// Expected a specific compound terminal but got something else.
26 ExpectedCompound3(TerminalToken, TerminalToken, TerminalToken),
27
28 /// Expected an identifier but got something else.
29 ///
30 /// # Example
31 /// ```text
32 /// global var !?!?! = "guh??"
33 /// ^ error
34 /// ```
35 ExpectedIdentifier,
36
37 /// Expected a literal but got something else.
38 ExpectedLiteral,
39
40 /// Expected a token that starts an expression but got something else.
41 ///
42 /// # Example
43 /// ```text
44 /// int What = globalize_all_functions
45 /// ^ error
46 /// ```
47 ExpectedExpression,
48
49 /// Expected a token that starts a value in an expression but got something else.
50 ///
51 /// # Example
52 /// ```text
53 /// local sum = 1 + ?
54 /// ^ error
55 /// ```
56 ExpectedValue,
57
58 /// Expected an operator but got something else.
59 ExpectedOperator,
60
61 /// Expected a prefix operator but got something else.
62 ExpectedPrefixOperator,
63
64 /// Expected a postfix operator but got something else.
65 ExpectedPostfixOperator,
66
67 /// Expected a binary operator but got something else.
68 ExpectedBinaryOperator,
69
70 /// Expected a type but got something else.
71 ///
72 /// # Example
73 /// ```text
74 /// typedef Five 5
75 /// ^ error
76 /// ```
77 ExpectedType,
78
79 /// Expected a type modifier but got something else.
80 ///
81 /// # Example
82 /// ```text
83 /// typedef help table&-
84 /// ^ error
85 /// ```
86 ExpectedTypeModifier,
87
88 /// Expected a token that starts a table slot but got something else.
89 ///
90 /// # Example
91 /// ```text
92 /// my_table = {
93 /// class MyTableClass {}
94 /// ^ error
95 /// }
96 /// ```
97 ExpectedTableSlot,
98
99 /// Expected a token that starts a class member but got something else.
100 ///
101 /// # Example
102 /// ```text
103 /// class MyClass {
104 /// globalize_all_functions
105 /// ^ error
106 /// }
107 /// ```
108 ExpectedClassMember,
109
110 /// Expected a token that starts a statement but got something else.
111 ///
112 /// # Example
113 /// ```text
114 /// > hey
115 /// ^ error
116 /// ```
117 ExpectedStatement,
118
119 /// Expected a newline or semicolon to end a statement but got something else.
120 ///
121 /// # Example
122 /// ```text
123 /// { 1 } + 2
124 /// ^ error
125 /// ```
126 ExpectedEndOfStatement,
127
128 /// Expected a token that starts a global definition but got something else.
129 ///
130 /// # Example
131 /// ```text
132 /// global if ()
133 /// ^ error
134 /// ```
135 ExpectedGlobalDefinition,
136
137 /// Found a linebreak in a place where one is not allowed.
138 IllegalLineBreak,
139
140 /// An expression was not allowed due to precedence rules.
141 Precedence,
142
143 /// Expected a slot in a class or table.
144 ExpectedSlot,
145
146 /// Expected a string literal.
147 ExpectedStringLiteral,
148}
149
150/// An error emitted while trying to parse a token list.
151///
152/// Each error has a type with more information, the token where the error occurred, and possibly
153/// some contextual information.
154#[derive(Debug, Clone)]
155pub struct ParseError {
156 /// The type of error.
157 pub ty: ParseErrorType,
158
159 /// The index of the token where the error occurred.
160 pub token_index: usize,
161
162 /// Affinity of the token.
163 pub token_affinity: TokenAffinity,
164
165 /// Contextual information if available.
166 pub context: Option<ParseErrorContext>,
167
168 pub(crate) is_fatal: bool,
169}
170
171/// Affinity of the token index in [`ParseError`].
172///
173/// This controls how a token range is printed when the token is at the start of a newline. For
174/// example, in this input:
175/// ```text
176/// a +
177/// b
178/// ```
179///
180/// An affinity of [`Before`] on `b` would highlight the end of the line before `b`, indicating the
181/// error is not necessarily related to `b` itself but to something missing after `+`:
182/// ```text
183/// a +
184/// ^
185/// b
186/// ```
187///
188/// An affinity of [`Inline`] would highlight `b` itself, indicating it is the problematic token:
189/// ```text
190/// a +
191/// b
192/// ^
193/// ```
194///
195/// [`Before`]: TokenAffinity::Before
196/// [`Inline`]: TokenAffinity::Inline
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum TokenAffinity {
199 Before,
200 Inline,
201}
202
203/// Context attached to a [`ParseError`].
204///
205/// This is generally attached to an error when the parser knows the context of what it is parsing
206/// with confidence.
207///
208/// # Example
209/// In this code, the parser knows that it is parsing the RHS of an expression when the error
210/// occurs.
211/// ```text
212/// 1 + function
213/// ^ error
214/// ```
215/// So it will attach a context to the error with an [`Expression`] context.
216///
217/// [`Expression`]: ContextType::Expression
218#[derive(Debug, Clone)]
219pub struct ParseErrorContext {
220 /// The range of tokens that this context applies.
221 ///
222 /// For example, if the context is a [`FunctionDefinitionStatement`], the range will include
223 /// the entire function.
224 ///
225 /// In some cases this will end at the token where the error is encountered, however in many
226 /// cases the parser can match delimiters like `{` and `}` to provide more context.
227 ///
228 /// [`FunctionDefinitionStatement`]: crate::ast::FunctionDefinitionStatement
229 pub token_range: Range<usize>,
230
231 /// Affinity of the last token in the range.
232 pub end_affinity: TokenAffinity,
233
234 /// The type of context.
235 pub ty: ContextType,
236}
237
238impl ParseError {
239 /// Creates a new `ParseError`.
240 pub fn new(ty: ParseErrorType, token_index: usize, token_affinity: TokenAffinity) -> Self {
241 ParseError {
242 ty,
243 token_index,
244 token_affinity,
245 context: None,
246 is_fatal: false,
247 }
248 }
249
250 /// Attaches some context to the error.
251 pub fn with_context(
252 self,
253 ty: ContextType,
254 token_range: Range<usize>,
255 end_affinity: TokenAffinity,
256 ) -> Self {
257 self.replace_context(ContextType::Span, ty, token_range, end_affinity)
258 }
259
260 /// Replaces an existing context with a new one, if it matches.
261 pub fn replace_context(
262 mut self,
263 from_ty: ContextType,
264 to_ty: ContextType,
265 token_range: Range<usize>,
266 end_affinity: TokenAffinity,
267 ) -> Self {
268 // Sanity check, ensure the range includes the actual token.
269 let token_range = (self.token_index.min(token_range.start))
270 ..((self.token_index + 1).max(token_range.end));
271
272 match &mut self.context {
273 // Set a new context if there isn't one already.
274 None => {
275 self.context = Some(ParseErrorContext {
276 token_range,
277 ty: to_ty,
278 end_affinity,
279 });
280 }
281
282 // Replace the existing context if it matches the replace type.
283 Some(context) if context.ty == from_ty => {
284 // Ensure the range contains both, allowing an inner context to expand the outer context.
285 let token_range = (token_range.start.min(context.token_range.start))
286 ..(token_range.end.max(context.token_range.end));
287 let end_affinity = if context.end_affinity == TokenAffinity::Inline {
288 TokenAffinity::Inline
289 } else {
290 end_affinity
291 };
292 *context = ParseErrorContext {
293 token_range,
294 ty: to_ty,
295 end_affinity,
296 };
297 }
298
299 // Otherwise, leave the existing context intact.
300 _ => {}
301 }
302
303 self
304 }
305
306 /// Returns an implementation of [`std::fmt::Display`] that pretty-prints the error and context
307 /// using [`display_annotations`].
308 pub fn display<'s>(
309 &'s self,
310 source: &'s str,
311 tokens: &'s [TokenItem<'s>],
312 file_name: Option<&'s str>,
313 ) -> impl std::fmt::Display + 's {
314 Display {
315 error: self,
316 source,
317 tokens,
318 file_name,
319 }
320 }
321
322 pub(crate) fn into_fatal(mut self) -> Self {
323 self.is_fatal = true;
324 self
325 }
326
327 pub(crate) fn into_non_fatal(mut self) -> Self {
328 self.is_fatal = false;
329 self
330 }
331}
332
333impl std::fmt::Display for ParseErrorType {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 match self {
336 ParseErrorType::ExpectedTerminal(terminal) => {
337 write!(f, "expected `{}`", terminal.as_str())
338 }
339 ParseErrorType::ExpectedCompound2(token1, token2) => {
340 write!(f, "expected `{}{}`", token1.as_str(), token2.as_str())
341 }
342 ParseErrorType::ExpectedCompound3(token1, token2, token3) => write!(
343 f,
344 "expected `{}{}{}`",
345 token1.as_str(),
346 token2.as_str(),
347 token3.as_str()
348 ),
349 ParseErrorType::ExpectedIdentifier => write!(f, "expected an identifier"),
350 ParseErrorType::ExpectedLiteral => write!(f, "expected a literal"),
351 ParseErrorType::ExpectedExpression => write!(f, "expected an expression"),
352 ParseErrorType::ExpectedValue => write!(f, "expected a value"),
353 ParseErrorType::ExpectedOperator => write!(f, "expected an operator"),
354 ParseErrorType::ExpectedPrefixOperator => write!(f, "expected a prefix operator"),
355 ParseErrorType::ExpectedPostfixOperator => write!(f, "expected a postfix operator"),
356 ParseErrorType::ExpectedBinaryOperator => write!(f, "expected a binary operator"),
357 ParseErrorType::ExpectedType => write!(f, "expected a type"),
358 ParseErrorType::ExpectedTypeModifier => write!(f, "expected a type modifier"),
359 ParseErrorType::ExpectedTableSlot => write!(f, "expected a table slot"),
360 ParseErrorType::ExpectedClassMember => write!(f, "expected a class member"),
361 ParseErrorType::ExpectedStatement => write!(f, "expected a statement"),
362 ParseErrorType::ExpectedEndOfStatement => {
363 write!(f, "expected a newline or `;`")
364 }
365 ParseErrorType::ExpectedGlobalDefinition => write!(f, "expected a global definition"),
366 ParseErrorType::ExpectedSlot => write!(f, "expected a slot"),
367 ParseErrorType::ExpectedStringLiteral => write!(f, "expected a string literal"),
368
369 // todo: these need rewording to fit with "<>, found a <>"
370 ParseErrorType::IllegalLineBreak => {
371 write!(f, "expected anything but `\\n`; got it anyway")
372 }
373 ParseErrorType::Precedence => write!(f, "not allowed due to precedence rules"),
374 }
375 }
376}
377
378struct Display<'s> {
379 error: &'s ParseError,
380 source: &'s str,
381 tokens: &'s [TokenItem<'s>],
382 file_name: Option<&'s str>,
383}
384
385impl std::fmt::Display for Display<'_> {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 write!(
388 f,
389 "{}{}{}",
390 Paint::red("error").bold(),
391 Paint::white(": ").bold(),
392 Paint::white(self.error.ty).bold()
393 )?;
394 match self.tokens.get(self.error.token_index) {
395 Some(item) => writeln!(
396 f,
397 "{}{}",
398 Paint::white(", found a ").bold(),
399 Paint::white(item.token.ty).bold()
400 )?,
401 None => writeln!(f, "{}", Paint::white(", found the end of input").bold())?,
402 }
403
404 let src_range = token_src_range(
405 self.error.token_index,
406 self.error.token_affinity,
407 self.tokens,
408 );
409 let note = match &self.error.context {
410 Some(context) => {
411 let is_end = self.error.token_index + 1 == context.token_range.end
412 && context.end_affinity == TokenAffinity::Before;
413 let context_text = if is_end { "for this" } else { "in this" };
414 format!("{} {}:", context_text, context.ty)
415 }
416 None => "".to_string(),
417 };
418
419 let mut annotations = vec![Annotation {
420 mode: Mode::Error,
421 text: format!("{}", self.error.ty),
422 note,
423 highlight: src_range.clone(),
424 visible: src_range.clone(),
425 }];
426
427 if let Some(context) = &self.error.context {
428 let start_range = token_src_range(
429 context.token_range.start,
430 TokenAffinity::Inline,
431 self.tokens,
432 );
433 let end_range = token_src_range(
434 context.token_range.end - 1,
435 context.end_affinity,
436 self.tokens,
437 );
438 annotations.push(Annotation {
439 mode: Mode::Info,
440 text: "".to_string(),
441 note: "".to_string(),
442 highlight: start_range.start..end_range.end,
443 visible: src_range,
444 });
445 }
446
447 write!(
448 f,
449 "{}",
450 display_annotations(self.file_name, self.source, &annotations)
451 )?;
452 Ok(())
453 }
454}
455
456fn token_src_range(
457 token_index: usize,
458 affinity: TokenAffinity,
459 tokens: &[TokenItem],
460) -> Range<usize> {
461 let Some(last_item) = tokens.last() else { return 0..0; };
462
463 if affinity == TokenAffinity::Before {
464 if token_index > 0 {
465 let last_item = &tokens[token_index.min(tokens.len()) - 1];
466 last_item.token.range.end..last_item.token.range.end
467 } else {
468 0..0
469 }
470 } else if token_index < tokens.len() {
471 let item = &tokens[token_index];
472 item.token.range.clone()
473 } else {
474 last_item.token.range.end..last_item.token.range.end
475 }
476}