pest/
error.rs

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