• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // pest. The Elegant Parser
2 // Copyright (c) 2018 Dragoș Tiselice
3 //
4 // Licensed under the Apache License, Version 2.0
5 // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6 // license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7 // option. All files in the project carrying such notice may not be copied,
8 // modified, or distributed except according to those terms.
9 
10 //! Types for different kinds of parsing failures.
11 
12 use std::cmp;
13 use std::error;
14 use std::fmt;
15 use std::mem;
16 
17 use position::Position;
18 use span::Span;
19 use RuleType;
20 
21 /// Parse-related error type.
22 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
23 pub struct Error<R> {
24     /// Variant of the error
25     pub variant: ErrorVariant<R>,
26     /// Location within the input string
27     pub location: InputLocation,
28     /// Line/column within the input string
29     pub line_col: LineColLocation,
30     path: Option<String>,
31     line: String,
32     continued_line: Option<String>,
33 }
34 
35 /// Different kinds of parsing errors.
36 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
37 pub enum ErrorVariant<R> {
38     /// Generated parsing error with expected and unexpected `Rule`s
39     ParsingError {
40         /// Positive attempts
41         positives: Vec<R>,
42         /// Negative attempts
43         negatives: Vec<R>,
44     },
45     /// Custom error with a message
46     CustomError {
47         /// Short explanation
48         message: String,
49     },
50 }
51 
52 /// Where an `Error` has occurred.
53 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
54 pub enum InputLocation {
55     /// `Error` was created by `Error::new_from_pos`
56     Pos(usize),
57     /// `Error` was created by `Error::new_from_span`
58     Span((usize, usize)),
59 }
60 
61 /// Line/column where an `Error` has occurred.
62 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
63 pub enum LineColLocation {
64     /// Line/column pair if `Error` was created by `Error::new_from_pos`
65     Pos((usize, usize)),
66     /// Line/column pairs if `Error` was created by `Error::new_from_span`
67     Span((usize, usize), (usize, usize)),
68 }
69 
70 impl<R: RuleType> Error<R> {
71     /// Creates `Error` from `ErrorVariant` and `Position`.
72     ///
73     /// # Examples
74     ///
75     /// ```
76     /// # use pest::error::{Error, ErrorVariant};
77     /// # use pest::Position;
78     /// # #[allow(non_camel_case_types)]
79     /// # #[allow(dead_code)]
80     /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81     /// # enum Rule {
82     /// #     open_paren,
83     /// #     closed_paren
84     /// # }
85     /// # let input = "";
86     /// # let pos = Position::from_start(input);
87     /// let error = Error::new_from_pos(
88     ///     ErrorVariant::ParsingError {
89     ///         positives: vec![Rule::open_paren],
90     ///         negatives: vec![Rule::closed_paren]
91     ///     },
92     ///     pos
93     /// );
94     ///
95     /// println!("{}", error);
96     /// ```
97     #[allow(clippy::needless_pass_by_value)]
new_from_pos(variant: ErrorVariant<R>, pos: Position) -> Error<R>98     pub fn new_from_pos(variant: ErrorVariant<R>, pos: Position) -> Error<R> {
99         Error {
100             variant,
101             location: InputLocation::Pos(pos.pos()),
102             path: None,
103             line: visualize_whitespace(pos.line_of()),
104             continued_line: None,
105             line_col: LineColLocation::Pos(pos.line_col()),
106         }
107     }
108 
109     /// Creates `Error` from `ErrorVariant` and `Span`.
110     ///
111     /// # Examples
112     ///
113     /// ```
114     /// # use pest::error::{Error, ErrorVariant};
115     /// # use pest::{Position, Span};
116     /// # #[allow(non_camel_case_types)]
117     /// # #[allow(dead_code)]
118     /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119     /// # enum Rule {
120     /// #     open_paren,
121     /// #     closed_paren
122     /// # }
123     /// # let input = "";
124     /// # let start = Position::from_start(input);
125     /// # let end = start.clone();
126     /// # let span = start.span(&end);
127     /// let error = Error::new_from_span(
128     ///     ErrorVariant::ParsingError {
129     ///         positives: vec![Rule::open_paren],
130     ///         negatives: vec![Rule::closed_paren]
131     ///     },
132     ///     span
133     /// );
134     ///
135     /// println!("{}", error);
136     /// ```
137     #[allow(clippy::needless_pass_by_value)]
new_from_span(variant: ErrorVariant<R>, span: Span) -> Error<R>138     pub fn new_from_span(variant: ErrorVariant<R>, span: Span) -> Error<R> {
139         let end = span.end_pos();
140 
141         let mut end_line_col = end.line_col();
142         // end position is after a \n, so we want to point to the visual lf symbol
143         if end_line_col.1 == 1 {
144             let mut visual_end = end.clone();
145             visual_end.skip_back(1);
146             let lc = visual_end.line_col();
147             end_line_col = (lc.0, lc.1 + 1);
148         };
149 
150         let mut line_iter = span.lines();
151         let start_line = visualize_whitespace(line_iter.next().unwrap_or(""));
152         let continued_line = line_iter.last().map(visualize_whitespace);
153 
154         Error {
155             variant,
156             location: InputLocation::Span((span.start(), end.pos())),
157             path: None,
158             line: start_line,
159             continued_line,
160             line_col: LineColLocation::Span(span.start_pos().line_col(), end_line_col),
161         }
162     }
163 
164     /// Returns `Error` variant with `path` which is shown when formatted with `Display`.
165     ///
166     /// # Examples
167     ///
168     /// ```
169     /// # use pest::error::{Error, ErrorVariant};
170     /// # use pest::Position;
171     /// # #[allow(non_camel_case_types)]
172     /// # #[allow(dead_code)]
173     /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174     /// # enum Rule {
175     /// #     open_paren,
176     /// #     closed_paren
177     /// # }
178     /// # let input = "";
179     /// # let pos = Position::from_start(input);
180     /// Error::new_from_pos(
181     ///     ErrorVariant::ParsingError {
182     ///         positives: vec![Rule::open_paren],
183     ///         negatives: vec![Rule::closed_paren]
184     ///     },
185     ///     pos
186     /// ).with_path("file.rs");
187     /// ```
with_path(mut self, path: &str) -> Error<R>188     pub fn with_path(mut self, path: &str) -> Error<R> {
189         self.path = Some(path.to_owned());
190 
191         self
192     }
193 
194     /// Renames all `Rule`s if this is a [`ParsingError`]. It does nothing when called on a
195     /// [`CustomError`].
196     ///
197     /// Useful in order to rename verbose rules or have detailed per-`Rule` formatting.
198     ///
199     /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError
200     /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError
201     ///
202     /// # Examples
203     ///
204     /// ```
205     /// # use pest::error::{Error, ErrorVariant};
206     /// # use pest::Position;
207     /// # #[allow(non_camel_case_types)]
208     /// # #[allow(dead_code)]
209     /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
210     /// # enum Rule {
211     /// #     open_paren,
212     /// #     closed_paren
213     /// # }
214     /// # let input = "";
215     /// # let pos = Position::from_start(input);
216     /// Error::new_from_pos(
217     ///     ErrorVariant::ParsingError {
218     ///         positives: vec![Rule::open_paren],
219     ///         negatives: vec![Rule::closed_paren]
220     ///     },
221     ///     pos
222     /// ).renamed_rules(|rule| {
223     ///     match *rule {
224     ///         Rule::open_paren => "(".to_owned(),
225     ///         Rule::closed_paren => "closed paren".to_owned()
226     ///     }
227     /// });
228     /// ```
renamed_rules<F>(mut self, f: F) -> Error<R> where F: FnMut(&R) -> String,229     pub fn renamed_rules<F>(mut self, f: F) -> Error<R>
230     where
231         F: FnMut(&R) -> String,
232     {
233         let variant = match self.variant {
234             ErrorVariant::ParsingError {
235                 positives,
236                 negatives,
237             } => {
238                 let message = Error::parsing_error_message(&positives, &negatives, f);
239                 ErrorVariant::CustomError { message }
240             }
241             variant => variant,
242         };
243 
244         self.variant = variant;
245 
246         self
247     }
248 
start(&self) -> (usize, usize)249     fn start(&self) -> (usize, usize) {
250         match self.line_col {
251             LineColLocation::Pos(line_col) => line_col,
252             LineColLocation::Span(start_line_col, _) => start_line_col,
253         }
254     }
255 
spacing(&self) -> String256     fn spacing(&self) -> String {
257         let line = match self.line_col {
258             LineColLocation::Pos((line, _)) => line,
259             LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line),
260         };
261 
262         let line_str_len = format!("{}", line).len();
263 
264         let mut spacing = String::new();
265         for _ in 0..line_str_len {
266             spacing.push(' ');
267         }
268 
269         spacing
270     }
271 
underline(&self) -> String272     fn underline(&self) -> String {
273         let mut underline = String::new();
274 
275         let mut start = self.start().1;
276         let end = match self.line_col {
277             LineColLocation::Span(_, (_, mut end)) => {
278                 let inverted_cols = start > end;
279                 if inverted_cols {
280                     mem::swap(&mut start, &mut end);
281                     start -= 1;
282                     end += 1;
283                 }
284 
285                 Some(end)
286             }
287             _ => None,
288         };
289         let offset = start - 1;
290         let line_chars = self.line.chars();
291 
292         for c in line_chars.take(offset) {
293             match c {
294                 '\t' => underline.push('\t'),
295                 _ => underline.push(' '),
296             }
297         }
298 
299         if let Some(end) = end {
300             if end - start > 1 {
301                 underline.push('^');
302                 for _ in 2..(end - start) {
303                     underline.push('-');
304                 }
305                 underline.push('^');
306             } else {
307                 underline.push('^');
308             }
309         } else {
310             underline.push_str("^---")
311         }
312 
313         underline
314     }
315 
message(&self) -> String316     fn message(&self) -> String {
317         match self.variant {
318             ErrorVariant::ParsingError {
319                 ref positives,
320                 ref negatives,
321             } => Error::parsing_error_message(positives, negatives, |r| format!("{:?}", r)),
322             ErrorVariant::CustomError { ref message } => message.clone(),
323         }
324     }
325 
parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String where F: FnMut(&R) -> String,326     fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String
327     where
328         F: FnMut(&R) -> String,
329     {
330         match (negatives.is_empty(), positives.is_empty()) {
331             (false, false) => format!(
332                 "unexpected {}; expected {}",
333                 Error::enumerate(negatives, &mut f),
334                 Error::enumerate(positives, &mut f)
335             ),
336             (false, true) => format!("unexpected {}", Error::enumerate(negatives, &mut f)),
337             (true, false) => format!("expected {}", Error::enumerate(positives, &mut f)),
338             (true, true) => "unknown parsing error".to_owned(),
339         }
340     }
341 
enumerate<F>(rules: &[R], f: &mut F) -> String where F: FnMut(&R) -> String,342     fn enumerate<F>(rules: &[R], f: &mut F) -> String
343     where
344         F: FnMut(&R) -> String,
345     {
346         match rules.len() {
347             1 => f(&rules[0]),
348             2 => format!("{} or {}", f(&rules[0]), f(&rules[1])),
349             l => {
350                 let separated = rules
351                     .iter()
352                     .take(l - 1)
353                     .map(|r| f(r))
354                     .collect::<Vec<_>>()
355                     .join(", ");
356                 format!("{}, or {}", separated, f(&rules[l - 1]))
357             }
358         }
359     }
360 
format(&self) -> String361     pub(crate) fn format(&self) -> String {
362         let spacing = self.spacing();
363         let path = self
364             .path
365             .as_ref()
366             .map(|path| format!("{}:", path))
367             .unwrap_or_default();
368 
369         let pair = (self.line_col.clone(), &self.continued_line);
370         if let (LineColLocation::Span(_, end), &Some(ref continued_line)) = pair {
371             let has_line_gap = end.0 - self.start().0 > 1;
372             if has_line_gap {
373                 format!(
374                     "{s    }--> {p}{ls}:{c}\n\
375                      {s    } |\n\
376                      {ls:w$} | {line}\n\
377                      {s    } | ...\n\
378                      {le:w$} | {continued_line}\n\
379                      {s    } | {underline}\n\
380                      {s    } |\n\
381                      {s    } = {message}",
382                     s = spacing,
383                     w = spacing.len(),
384                     p = path,
385                     ls = self.start().0,
386                     le = end.0,
387                     c = self.start().1,
388                     line = self.line,
389                     continued_line = continued_line,
390                     underline = self.underline(),
391                     message = self.message()
392                 )
393             } else {
394                 format!(
395                     "{s    }--> {p}{ls}:{c}\n\
396                      {s    } |\n\
397                      {ls:w$} | {line}\n\
398                      {le:w$} | {continued_line}\n\
399                      {s    } | {underline}\n\
400                      {s    } |\n\
401                      {s    } = {message}",
402                     s = spacing,
403                     w = spacing.len(),
404                     p = path,
405                     ls = self.start().0,
406                     le = end.0,
407                     c = self.start().1,
408                     line = self.line,
409                     continued_line = continued_line,
410                     underline = self.underline(),
411                     message = self.message()
412                 )
413             }
414         } else {
415             format!(
416                 "{s}--> {p}{l}:{c}\n\
417                  {s} |\n\
418                  {l} | {line}\n\
419                  {s} | {underline}\n\
420                  {s} |\n\
421                  {s} = {message}",
422                 s = spacing,
423                 p = path,
424                 l = self.start().0,
425                 c = self.start().1,
426                 line = self.line,
427                 underline = self.underline(),
428                 message = self.message()
429             )
430         }
431     }
432 }
433 
434 impl<R: RuleType> fmt::Display for Error<R> {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result435     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
436         write!(f, "{}", self.format())
437     }
438 }
439 
440 impl<'i, R: RuleType> error::Error for Error<R> {
description(&self) -> &str441     fn description(&self) -> &str {
442         match self.variant {
443             ErrorVariant::ParsingError { .. } => "parsing error",
444             ErrorVariant::CustomError { ref message } => message,
445         }
446     }
447 }
448 
visualize_whitespace(input: &str) -> String449 fn visualize_whitespace(input: &str) -> String {
450     input.to_owned().replace('\r', "␍").replace('\n', "␊")
451 }
452 
453 #[cfg(test)]
454 mod tests {
455     use super::super::position;
456     use super::*;
457 
458     #[test]
display_parsing_error_mixed()459     fn display_parsing_error_mixed() {
460         let input = "ab\ncd\nef";
461         let pos = position::Position::new(input, 4).unwrap();
462         let error: Error<u32> = Error::new_from_pos(
463             ErrorVariant::ParsingError {
464                 positives: vec![1, 2, 3],
465                 negatives: vec![4, 5, 6],
466             },
467             pos,
468         );
469 
470         assert_eq!(
471             format!("{}", error),
472             vec![
473                 " --> 2:2",
474                 "  |",
475                 "2 | cd␊",
476                 "  |  ^---",
477                 "  |",
478                 "  = unexpected 4, 5, or 6; expected 1, 2, or 3",
479             ]
480             .join("\n")
481         );
482     }
483 
484     #[test]
display_parsing_error_positives()485     fn display_parsing_error_positives() {
486         let input = "ab\ncd\nef";
487         let pos = position::Position::new(input, 4).unwrap();
488         let error: Error<u32> = Error::new_from_pos(
489             ErrorVariant::ParsingError {
490                 positives: vec![1, 2],
491                 negatives: vec![],
492             },
493             pos,
494         );
495 
496         assert_eq!(
497             format!("{}", error),
498             vec![
499                 " --> 2:2",
500                 "  |",
501                 "2 | cd␊",
502                 "  |  ^---",
503                 "  |",
504                 "  = expected 1 or 2",
505             ]
506             .join("\n")
507         );
508     }
509 
510     #[test]
display_parsing_error_negatives()511     fn display_parsing_error_negatives() {
512         let input = "ab\ncd\nef";
513         let pos = position::Position::new(input, 4).unwrap();
514         let error: Error<u32> = Error::new_from_pos(
515             ErrorVariant::ParsingError {
516                 positives: vec![],
517                 negatives: vec![4, 5, 6],
518             },
519             pos,
520         );
521 
522         assert_eq!(
523             format!("{}", error),
524             vec![
525                 " --> 2:2",
526                 "  |",
527                 "2 | cd␊",
528                 "  |  ^---",
529                 "  |",
530                 "  = unexpected 4, 5, or 6",
531             ]
532             .join("\n")
533         );
534     }
535 
536     #[test]
display_parsing_error_unknown()537     fn display_parsing_error_unknown() {
538         let input = "ab\ncd\nef";
539         let pos = position::Position::new(input, 4).unwrap();
540         let error: Error<u32> = Error::new_from_pos(
541             ErrorVariant::ParsingError {
542                 positives: vec![],
543                 negatives: vec![],
544             },
545             pos,
546         );
547 
548         assert_eq!(
549             format!("{}", error),
550             vec![
551                 " --> 2:2",
552                 "  |",
553                 "2 | cd␊",
554                 "  |  ^---",
555                 "  |",
556                 "  = unknown parsing error",
557             ]
558             .join("\n")
559         );
560     }
561 
562     #[test]
display_custom_pos()563     fn display_custom_pos() {
564         let input = "ab\ncd\nef";
565         let pos = position::Position::new(input, 4).unwrap();
566         let error: Error<u32> = Error::new_from_pos(
567             ErrorVariant::CustomError {
568                 message: "error: big one".to_owned(),
569             },
570             pos,
571         );
572 
573         assert_eq!(
574             format!("{}", error),
575             vec![
576                 " --> 2:2",
577                 "  |",
578                 "2 | cd␊",
579                 "  |  ^---",
580                 "  |",
581                 "  = error: big one",
582             ]
583             .join("\n")
584         );
585     }
586 
587     #[test]
display_custom_span_two_lines()588     fn display_custom_span_two_lines() {
589         let input = "ab\ncd\nefgh";
590         let start = position::Position::new(input, 4).unwrap();
591         let end = position::Position::new(input, 9).unwrap();
592         let error: Error<u32> = Error::new_from_span(
593             ErrorVariant::CustomError {
594                 message: "error: big one".to_owned(),
595             },
596             start.span(&end),
597         );
598 
599         assert_eq!(
600             format!("{}", error),
601             vec![
602                 " --> 2:2",
603                 "  |",
604                 "2 | cd␊",
605                 "3 | efgh",
606                 "  |  ^^",
607                 "  |",
608                 "  = error: big one",
609             ]
610             .join("\n")
611         );
612     }
613 
614     #[test]
display_custom_span_three_lines()615     fn display_custom_span_three_lines() {
616         let input = "ab\ncd\nefgh";
617         let start = position::Position::new(input, 1).unwrap();
618         let end = position::Position::new(input, 9).unwrap();
619         let error: Error<u32> = Error::new_from_span(
620             ErrorVariant::CustomError {
621                 message: "error: big one".to_owned(),
622             },
623             start.span(&end),
624         );
625 
626         assert_eq!(
627             format!("{}", error),
628             vec![
629                 " --> 1:2",
630                 "  |",
631                 "1 | ab␊",
632                 "  | ...",
633                 "3 | efgh",
634                 "  |  ^^",
635                 "  |",
636                 "  = error: big one",
637             ]
638             .join("\n")
639         );
640     }
641 
642     #[test]
display_custom_span_two_lines_inverted_cols()643     fn display_custom_span_two_lines_inverted_cols() {
644         let input = "abcdef\ngh";
645         let start = position::Position::new(input, 5).unwrap();
646         let end = position::Position::new(input, 8).unwrap();
647         let error: Error<u32> = Error::new_from_span(
648             ErrorVariant::CustomError {
649                 message: "error: big one".to_owned(),
650             },
651             start.span(&end),
652         );
653 
654         assert_eq!(
655             format!("{}", error),
656             vec![
657                 " --> 1:6",
658                 "  |",
659                 "1 | abcdef␊",
660                 "2 | gh",
661                 "  | ^----^",
662                 "  |",
663                 "  = error: big one",
664             ]
665             .join("\n")
666         );
667     }
668 
669     #[test]
display_custom_span_end_after_newline()670     fn display_custom_span_end_after_newline() {
671         let input = "abcdef\n";
672         let start = position::Position::new(input, 0).unwrap();
673         let end = position::Position::new(input, 7).unwrap();
674         assert!(start.at_start());
675         assert!(end.at_end());
676 
677         let error: Error<u32> = Error::new_from_span(
678             ErrorVariant::CustomError {
679                 message: "error: big one".to_owned(),
680             },
681             start.span(&end),
682         );
683 
684         assert_eq!(
685             format!("{}", error),
686             vec![
687                 " --> 1:1",
688                 "  |",
689                 "1 | abcdef␊",
690                 "  | ^-----^",
691                 "  |",
692                 "  = error: big one",
693             ]
694             .join("\n")
695         );
696     }
697 
698     #[test]
display_custom_span_empty()699     fn display_custom_span_empty() {
700         let input = "";
701         let start = position::Position::new(input, 0).unwrap();
702         let end = position::Position::new(input, 0).unwrap();
703         assert!(start.at_start());
704         assert!(end.at_end());
705 
706         let error: Error<u32> = Error::new_from_span(
707             ErrorVariant::CustomError {
708                 message: "error: empty".to_owned(),
709             },
710             start.span(&end),
711         );
712 
713         assert_eq!(
714             format!("{}", error),
715             vec![
716                 " --> 1:1",
717                 "  |",
718                 "1 | ",
719                 "  | ^",
720                 "  |",
721                 "  = error: empty",
722             ]
723             .join("\n")
724         );
725     }
726 
727     #[test]
mapped_parsing_error()728     fn mapped_parsing_error() {
729         let input = "ab\ncd\nef";
730         let pos = position::Position::new(input, 4).unwrap();
731         let error: Error<u32> = Error::new_from_pos(
732             ErrorVariant::ParsingError {
733                 positives: vec![1, 2, 3],
734                 negatives: vec![4, 5, 6],
735             },
736             pos,
737         )
738         .renamed_rules(|n| format!("{}", n + 1));
739 
740         assert_eq!(
741             format!("{}", error),
742             vec![
743                 " --> 2:2",
744                 "  |",
745                 "2 | cd␊",
746                 "  |  ^---",
747                 "  |",
748                 "  = unexpected 5, 6, or 7; expected 2, 3, or 4",
749             ]
750             .join("\n")
751         );
752     }
753 
754     #[test]
error_with_path()755     fn error_with_path() {
756         let input = "ab\ncd\nef";
757         let pos = position::Position::new(input, 4).unwrap();
758         let error: Error<u32> = Error::new_from_pos(
759             ErrorVariant::ParsingError {
760                 positives: vec![1, 2, 3],
761                 negatives: vec![4, 5, 6],
762             },
763             pos,
764         )
765         .with_path("file.rs");
766 
767         assert_eq!(
768             format!("{}", error),
769             vec![
770                 " --> file.rs:2:2",
771                 "  |",
772                 "2 | cd␊",
773                 "  |  ^---",
774                 "  |",
775                 "  = unexpected 4, 5, or 6; expected 1, 2, or 3",
776             ]
777             .join("\n")
778         );
779     }
780 
781     #[test]
underline_with_tabs()782     fn underline_with_tabs() {
783         let input = "a\txbc";
784         let pos = position::Position::new(input, 2).unwrap();
785         let error: Error<u32> = Error::new_from_pos(
786             ErrorVariant::ParsingError {
787                 positives: vec![1, 2, 3],
788                 negatives: vec![4, 5, 6],
789             },
790             pos,
791         )
792         .with_path("file.rs");
793 
794         assert_eq!(
795             format!("{}", error),
796             vec![
797                 " --> file.rs:1:3",
798                 "  |",
799                 "1 | a	xbc",
800                 "  |  	^---",
801                 "  |",
802                 "  = unexpected 4, 5, or 6; expected 1, 2, or 3",
803             ]
804             .join("\n")
805         );
806     }
807 }
808