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 crate::parser_state::{ParseAttempts, ParsingToken, RulesCallStack};
13use alloc::borrow::Cow;
14use alloc::borrow::ToOwned;
15use alloc::boxed::Box;
16use alloc::collections::{BTreeMap, BTreeSet};
17use alloc::format;
18use alloc::string::String;
19use alloc::string::ToString;
20use alloc::vec;
21use alloc::vec::Vec;
22use core::cmp;
23use core::fmt;
24use core::mem;
25
26use crate::position::Position;
27use crate::span::Span;
28use crate::RuleType;
29
30/// Parse-related error type.
31#[derive(Clone, Debug, Eq, Hash, PartialEq)]
32pub struct Error<R> {
33    /// Variant of the error
34    pub variant: ErrorVariant<R>,
35    /// Location within the input string
36    pub location: InputLocation,
37    /// Line/column within the input string
38    pub line_col: LineColLocation,
39    inner: Box<ErrorInner<R>>,
40}
41
42/// Private information for parse errors.
43#[derive(Clone, Debug, Eq, Hash, PartialEq)]
44struct ErrorInner<R> {
45    path: Option<String>,
46    line: String,
47    continued_line: Option<String>,
48    parse_attempts: Option<ParseAttempts<R>>,
49}
50
51impl<R: RuleType> core::error::Error for Error<R> {}
52
53/// Different kinds of parsing errors.
54#[derive(Clone, Debug, Eq, Hash, PartialEq)]
55pub enum ErrorVariant<R> {
56    /// Generated parsing error with expected and unexpected `Rule`s
57    ParsingError {
58        /// Positive attempts
59        positives: Vec<R>,
60        /// Negative attempts
61        negatives: Vec<R>,
62    },
63    /// Custom error with a message
64    CustomError {
65        /// Short explanation
66        message: String,
67    },
68}
69
70impl<R: RuleType> core::error::Error for ErrorVariant<R> {}
71
72/// Where an `Error` has occurred.
73#[derive(Clone, Debug, Eq, Hash, PartialEq)]
74pub enum InputLocation {
75    /// `Error` was created by `Error::new_from_pos`
76    Pos(usize),
77    /// `Error` was created by `Error::new_from_span`
78    Span((usize, usize)),
79}
80
81/// Line/column where an `Error` has occurred.
82#[derive(Clone, Debug, Eq, Hash, PartialEq)]
83pub enum LineColLocation {
84    /// Line/column pair if `Error` was created by `Error::new_from_pos`
85    Pos((usize, usize)),
86    /// Line/column pairs if `Error` was created by `Error::new_from_span`
87    Span((usize, usize), (usize, usize)),
88}
89
90impl From<Position<'_>> for LineColLocation {
91    fn from(value: Position<'_>) -> Self {
92        Self::Pos(value.line_col())
93    }
94}
95
96impl From<Span<'_>> for LineColLocation {
97    fn from(value: Span<'_>) -> Self {
98        let (start, end) = value.split();
99        Self::Span(start.line_col(), end.line_col())
100    }
101}
102
103/// Function mapping rule to its helper message defined by user.
104pub type RuleToMessageFn<R> = Box<dyn Fn(&R) -> Option<String>>;
105/// Function mapping string element to bool denoting whether it's a whitespace defined by user.
106pub type IsWhitespaceFn = Box<dyn Fn(String) -> bool>;
107
108impl ParsingToken {
109    pub fn is_whitespace(&self, is_whitespace: &IsWhitespaceFn) -> bool {
110        match self {
111            ParsingToken::Sensitive { token } => is_whitespace(token.clone()),
112            ParsingToken::Insensitive { token } => is_whitespace(token.clone()),
113            ParsingToken::Range { .. } => false,
114            ParsingToken::BuiltInRule => false,
115        }
116    }
117}
118
119impl<R: RuleType> ParseAttempts<R> {
120    /// Helper formatting function to get message informing about tokens we've
121    /// (un)expected to see.
122    /// Used as a part of `parse_attempts_error`.
123    fn tokens_helper_messages(
124        &self,
125        is_whitespace_fn: &IsWhitespaceFn,
126        spacing: &str,
127    ) -> Vec<String> {
128        let mut helper_messages = Vec::new();
129        let tokens_header_pairs = vec![
130            (self.expected_tokens(), "expected"),
131            (self.unexpected_tokens(), "unexpected"),
132        ];
133
134        for (tokens, header) in &tokens_header_pairs {
135            if tokens.is_empty() {
136                continue;
137            }
138
139            let mut helper_tokens_message = format!("{spacing}note: {header} ");
140            helper_tokens_message.push_str(if tokens.len() == 1 {
141                "token: "
142            } else {
143                "one of tokens: "
144            });
145
146            let expected_tokens_set: BTreeSet<String> = tokens
147                .iter()
148                .map(|token| {
149                    if token.is_whitespace(is_whitespace_fn) {
150                        String::from("WHITESPACE")
151                    } else {
152                        format!("`{}`", token)
153                    }
154                })
155                .collect();
156
157            helper_tokens_message.push_str(
158                &expected_tokens_set
159                    .iter()
160                    .cloned()
161                    .collect::<Vec<String>>()
162                    .join(", "),
163            );
164            helper_messages.push(helper_tokens_message);
165        }
166
167        helper_messages
168    }
169}
170
171impl<R: RuleType> Error<R> {
172    /// Creates `Error` from `ErrorVariant` and `Position`.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// # use pest::error::{Error, ErrorVariant};
178    /// # use pest::Position;
179    /// # #[allow(non_camel_case_types)]
180    /// # #[allow(dead_code)]
181    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
182    /// # enum Rule {
183    /// #     open_paren,
184    /// #     closed_paren
185    /// # }
186    /// # let input = "";
187    /// # let pos = Position::from_start(input);
188    /// let error = Error::new_from_pos(
189    ///     ErrorVariant::ParsingError {
190    ///         positives: vec![Rule::open_paren],
191    ///         negatives: vec![Rule::closed_paren],
192    ///     },
193    ///     pos
194    /// );
195    ///
196    /// println!("{}", error);
197    /// ```
198    pub fn new_from_pos(variant: ErrorVariant<R>, pos: Position<'_>) -> Error<R> {
199        let visualize_ws = pos.match_char('\n') || pos.match_char('\r');
200        let line_of = pos.line_of();
201        let line = if visualize_ws {
202            visualize_whitespace(line_of)
203        } else {
204            line_of.replace(&['\r', '\n'][..], "")
205        };
206        Error {
207            variant,
208            location: InputLocation::Pos(pos.pos()),
209            line_col: LineColLocation::Pos(pos.line_col()),
210            inner: Box::new(ErrorInner {
211                path: None,
212                line,
213                continued_line: None,
214                parse_attempts: None,
215            }),
216        }
217    }
218
219    /// Wrapper function to track `parse_attempts` as a result
220    /// of `state` function call in `parser_state.rs`.
221    pub(crate) fn new_from_pos_with_parsing_attempts(
222        variant: ErrorVariant<R>,
223        pos: Position<'_>,
224        parse_attempts: ParseAttempts<R>,
225    ) -> Error<R> {
226        let mut error = Self::new_from_pos(variant, pos);
227        error.inner.parse_attempts = Some(parse_attempts);
228        error
229    }
230
231    /// Creates `Error` from `ErrorVariant` and `Span`.
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// # use pest::error::{Error, ErrorVariant};
237    /// # use pest::{Position, Span};
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 start = Position::from_start(input);
247    /// # let end = start.clone();
248    /// # let span = start.span(&end);
249    /// let error = Error::new_from_span(
250    ///     ErrorVariant::ParsingError {
251    ///         positives: vec![Rule::open_paren],
252    ///         negatives: vec![Rule::closed_paren],
253    ///     },
254    ///     span
255    /// );
256    ///
257    /// println!("{}", error);
258    /// ```
259    pub fn new_from_span(variant: ErrorVariant<R>, span: Span<'_>) -> Error<R> {
260        let end = span.end_pos();
261        let mut end_line_col = end.line_col();
262        // end position is after a \n, so we want to point to the visual lf symbol
263        if end_line_col.1 == 1 {
264            let mut visual_end = end;
265            visual_end.skip_back(1);
266            let lc = visual_end.line_col();
267            end_line_col = (lc.0, lc.1 + 1);
268        };
269
270        let mut line_iter = span.lines();
271        let sl = line_iter.next().unwrap_or("");
272        let mut chars = span.as_str().chars();
273        let visualize_ws = matches!(chars.next(), Some('\n') | Some('\r'))
274            || matches!(chars.last(), Some('\n') | Some('\r'));
275        let start_line = if visualize_ws {
276            visualize_whitespace(sl)
277        } else {
278            sl.to_owned().replace(&['\r', '\n'][..], "")
279        };
280        let ll = line_iter.last();
281        let continued_line = if visualize_ws {
282            ll.map(str::to_owned)
283        } else {
284            ll.map(visualize_whitespace)
285        };
286
287        Error {
288            variant,
289            location: InputLocation::Span((span.start(), end.pos())),
290            line_col: LineColLocation::Span(span.start_pos().line_col(), end_line_col),
291            inner: Box::new(ErrorInner {
292                path: None,
293                line: start_line,
294                continued_line,
295                parse_attempts: None,
296            }),
297        }
298    }
299
300    /// Returns `Error` variant with `path` which is shown when formatted with `Display`.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// # use pest::error::{Error, ErrorVariant};
306    /// # use pest::Position;
307    /// # #[allow(non_camel_case_types)]
308    /// # #[allow(dead_code)]
309    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
310    /// # enum Rule {
311    /// #     open_paren,
312    /// #     closed_paren
313    /// # }
314    /// # let input = "";
315    /// # let pos = Position::from_start(input);
316    /// Error::new_from_pos(
317    ///     ErrorVariant::ParsingError {
318    ///         positives: vec![Rule::open_paren],
319    ///         negatives: vec![Rule::closed_paren],
320    ///     },
321    ///     pos
322    /// ).with_path("file.rs");
323    /// ```
324    pub fn with_path(mut self, path: &str) -> Error<R> {
325        self.inner.path = Some(path.to_owned());
326
327        self
328    }
329
330    /// Returns the path set using [`Error::with_path()`].
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// # use pest::error::{Error, ErrorVariant};
336    /// # use pest::Position;
337    /// # #[allow(non_camel_case_types)]
338    /// # #[allow(dead_code)]
339    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
340    /// # enum Rule {
341    /// #     open_paren,
342    /// #     closed_paren
343    /// # }
344    /// # let input = "";
345    /// # let pos = Position::from_start(input);
346    /// # let error = Error::new_from_pos(
347    /// #     ErrorVariant::ParsingError {
348    /// #         positives: vec![Rule::open_paren],
349    /// #         negatives: vec![Rule::closed_paren],
350    /// #     },
351    /// #     pos);
352    /// let error = error.with_path("file.rs");
353    /// assert_eq!(Some("file.rs"), error.path());
354    /// ```
355    pub fn path(&self) -> Option<&str> {
356        self.inner.path.as_deref()
357    }
358
359    /// Returns the line that the error is on.
360    pub fn line(&self) -> &str {
361        self.inner.line.as_str()
362    }
363
364    /// Renames all `Rule`s if this is a [`ParsingError`]. It does nothing when called on a
365    /// [`CustomError`].
366    ///
367    /// Useful in order to rename verbose rules or have detailed per-`Rule` formatting.
368    ///
369    /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError
370    /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError
371    ///
372    /// # Examples
373    ///
374    /// ```
375    /// # use pest::error::{Error, ErrorVariant};
376    /// # use pest::Position;
377    /// # #[allow(non_camel_case_types)]
378    /// # #[allow(dead_code)]
379    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
380    /// # enum Rule {
381    /// #     open_paren,
382    /// #     closed_paren
383    /// # }
384    /// # let input = "";
385    /// # let pos = Position::from_start(input);
386    /// Error::new_from_pos(
387    ///     ErrorVariant::ParsingError {
388    ///         positives: vec![Rule::open_paren],
389    ///         negatives: vec![Rule::closed_paren],
390    ///     },
391    ///     pos
392    /// ).renamed_rules(|rule| {
393    ///     match *rule {
394    ///         Rule::open_paren => "(".to_owned(),
395    ///         Rule::closed_paren => "closed paren".to_owned()
396    ///     }
397    /// });
398    /// ```
399    pub fn renamed_rules<F>(mut self, f: F) -> Error<R>
400    where
401        F: FnMut(&R) -> String,
402    {
403        let variant = match self.variant {
404            ErrorVariant::ParsingError {
405                positives,
406                negatives,
407            } => {
408                let message = Error::parsing_error_message(&positives, &negatives, f);
409                ErrorVariant::CustomError { message }
410            }
411            variant => variant,
412        };
413
414        self.variant = variant;
415
416        self
417    }
418
419    /// Get detailed information about errored rules sequence.
420    /// Returns `Some(results)` only for `ParsingError`.
421    pub fn parse_attempts(&self) -> Option<ParseAttempts<R>> {
422        self.inner.parse_attempts.clone()
423    }
424
425    /// Get error message based on parsing attempts.
426    /// Returns `None` in case self `parse_attempts` is `None`.
427    pub fn parse_attempts_error(
428        &self,
429        input: &str,
430        rule_to_message: &RuleToMessageFn<R>,
431        is_whitespace: &IsWhitespaceFn,
432    ) -> Option<Error<R>> {
433        let attempts = if let Some(ref parse_attempts) = self.inner.parse_attempts {
434            parse_attempts.clone()
435        } else {
436            return None;
437        };
438
439        let spacing = self.spacing() + "   ";
440        let error_position = attempts.max_position;
441        let message = {
442            let mut help_lines: Vec<String> = Vec::new();
443            help_lines.push(String::from("error: parsing error occurred."));
444
445            // Note: at least one of `(un)expected_tokens` must not be empty.
446            for tokens_helper_message in attempts.tokens_helper_messages(is_whitespace, &spacing) {
447                help_lines.push(tokens_helper_message);
448            }
449
450            let call_stacks = attempts.call_stacks();
451            // Group call stacks by their parents so that we can print common header and
452            // several sub helper messages.
453            let mut call_stacks_parents_groups: BTreeMap<Option<R>, Vec<RulesCallStack<R>>> =
454                BTreeMap::new();
455            for call_stack in call_stacks {
456                call_stacks_parents_groups
457                    .entry(call_stack.parent)
458                    .or_default()
459                    .push(call_stack);
460            }
461
462            for (group_parent, group) in call_stacks_parents_groups {
463                if let Some(parent_rule) = group_parent {
464                    let mut contains_meaningful_info = false;
465                    help_lines.push(format!(
466                        "{spacing}help: {}",
467                        if let Some(message) = rule_to_message(&parent_rule) {
468                            contains_meaningful_info = true;
469                            message
470                        } else {
471                            String::from("[Unknown parent rule]")
472                        }
473                    ));
474                    for call_stack in group {
475                        if let Some(r) = call_stack.deepest.get_rule() {
476                            if let Some(message) = rule_to_message(r) {
477                                contains_meaningful_info = true;
478                                help_lines.push(format!("{spacing}      - {message}"));
479                            }
480                        }
481                    }
482                    if !contains_meaningful_info {
483                        // Have to remove useless line for unknown parent rule.
484                        help_lines.pop();
485                    }
486                } else {
487                    for call_stack in group {
488                        // Note that `deepest` rule may be `None`. E.g. in case it corresponds
489                        // to WHITESPACE expected token which has no parent rule (on the top level
490                        // parsing).
491                        if let Some(r) = call_stack.deepest.get_rule() {
492                            let helper_message = rule_to_message(r);
493                            if let Some(helper_message) = helper_message {
494                                help_lines.push(format!("{spacing}help: {helper_message}"));
495                            }
496                        }
497                    }
498                }
499            }
500
501            help_lines.join("\n")
502        };
503        let error = Error::new_from_pos(
504            ErrorVariant::CustomError { message },
505            Position::new_internal(input, error_position),
506        );
507        Some(error)
508    }
509
510    fn start(&self) -> (usize, usize) {
511        match self.line_col {
512            LineColLocation::Pos(line_col) => line_col,
513            LineColLocation::Span(start_line_col, _) => start_line_col,
514        }
515    }
516
517    fn spacing(&self) -> String {
518        let line = match self.line_col {
519            LineColLocation::Pos((line, _)) => line,
520            LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line),
521        };
522
523        let line_str_len = format!("{}", line).len();
524
525        let mut spacing = String::new();
526        for _ in 0..line_str_len {
527            spacing.push(' ');
528        }
529
530        spacing
531    }
532
533    fn underline(&self) -> String {
534        let mut underline = String::new();
535
536        let mut start = self.start().1;
537        let end = match self.line_col {
538            LineColLocation::Span(_, (_, mut end)) => {
539                let inverted_cols = start > end;
540                if inverted_cols {
541                    mem::swap(&mut start, &mut end);
542                    start -= 1;
543                    end += 1;
544                }
545
546                Some(end)
547            }
548            _ => None,
549        };
550        let offset = start - 1;
551        let line_chars = self.inner.line.chars();
552
553        for c in line_chars.take(offset) {
554            match c {
555                '\t' => underline.push('\t'),
556                _ => underline.push(' '),
557            }
558        }
559
560        if let Some(end) = end {
561            underline.push('^');
562            if end - start > 1 {
563                for _ in 2..(end - start) {
564                    underline.push('-');
565                }
566                underline.push('^');
567            }
568        } else {
569            underline.push_str("^---")
570        }
571
572        underline
573    }
574
575    fn message(&self) -> String {
576        self.variant.message().to_string()
577    }
578
579    fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String
580    where
581        F: FnMut(&R) -> String,
582    {
583        match (negatives.is_empty(), positives.is_empty()) {
584            (false, false) => format!(
585                "unexpected {}; expected {}",
586                Error::enumerate(negatives, &mut f),
587                Error::enumerate(positives, &mut f)
588            ),
589            (false, true) => format!("unexpected {}", Error::enumerate(negatives, &mut f)),
590            (true, false) => format!("expected {}", Error::enumerate(positives, &mut f)),
591            (true, true) => "unknown parsing error".to_owned(),
592        }
593    }
594
595    fn enumerate<F>(rules: &[R], f: &mut F) -> String
596    where
597        F: FnMut(&R) -> String,
598    {
599        match rules.len() {
600            1 => f(&rules[0]),
601            2 => format!("{} or {}", f(&rules[0]), f(&rules[1])),
602            l => {
603                let non_separated = f(&rules[l - 1]);
604                let separated = rules
605                    .iter()
606                    .take(l - 1)
607                    .map(f)
608                    .collect::<Vec<_>>()
609                    .join(", ");
610                format!("{}, or {}", separated, non_separated)
611            }
612        }
613    }
614
615    pub(crate) fn format(&self) -> String {
616        let spacing = self.spacing();
617        let path = self
618            .inner
619            .path
620            .as_ref()
621            .map(|path| format!("{}:", path))
622            .unwrap_or_default();
623
624        let pair = (self.line_col.clone(), &self.inner.continued_line);
625        if let (LineColLocation::Span(_, end), Some(ref continued_line)) = pair {
626            let has_line_gap = end.0 - self.start().0 > 1;
627            if has_line_gap {
628                format!(
629                    "{s    }--> {p}{ls}:{c}\n\
630                     {s    } |\n\
631                     {ls:w$} | {line}\n\
632                     {s    } | ...\n\
633                     {le:w$} | {continued_line}\n\
634                     {s    } | {underline}\n\
635                     {s    } |\n\
636                     {s    } = {message}",
637                    s = spacing,
638                    w = spacing.len(),
639                    p = path,
640                    ls = self.start().0,
641                    le = end.0,
642                    c = self.start().1,
643                    line = self.inner.line,
644                    continued_line = continued_line,
645                    underline = self.underline(),
646                    message = self.message()
647                )
648            } else {
649                format!(
650                    "{s    }--> {p}{ls}:{c}\n\
651                     {s    } |\n\
652                     {ls:w$} | {line}\n\
653                     {le:w$} | {continued_line}\n\
654                     {s    } | {underline}\n\
655                     {s    } |\n\
656                     {s    } = {message}",
657                    s = spacing,
658                    w = spacing.len(),
659                    p = path,
660                    ls = self.start().0,
661                    le = end.0,
662                    c = self.start().1,
663                    line = self.inner.line,
664                    continued_line = continued_line,
665                    underline = self.underline(),
666                    message = self.message()
667                )
668            }
669        } else {
670            format!(
671                "{s}--> {p}{l}:{c}\n\
672                 {s} |\n\
673                 {l} | {line}\n\
674                 {s} | {underline}\n\
675                 {s} |\n\
676                 {s} = {message}",
677                s = spacing,
678                p = path,
679                l = self.start().0,
680                c = self.start().1,
681                line = self.inner.line,
682                underline = self.underline(),
683                message = self.message()
684            )
685        }
686    }
687
688    #[cfg(feature = "miette-error")]
689    /// Turns an error into a [miette](crates.io/miette) Diagnostic.
690    pub fn into_miette(self) -> impl ::miette::Diagnostic {
691        miette_adapter::MietteAdapter(self)
692    }
693}
694
695impl<R: RuleType> ErrorVariant<R> {
696    ///
697    /// Returns the error message for [`ErrorVariant`]
698    ///
699    /// If [`ErrorVariant`] is [`CustomError`], it returns a
700    /// [`Cow::Borrowed`] reference to [`message`]. If [`ErrorVariant`] is [`ParsingError`], a
701    /// [`Cow::Owned`] containing "expected [ErrorVariant::ParsingError::positives] [ErrorVariant::ParsingError::negatives]" is returned.
702    ///
703    /// [`ErrorVariant`]: enum.ErrorVariant.html
704    /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError
705    /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError
706    /// [`Cow::Owned`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Owned
707    /// [`Cow::Borrowed`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Borrowed
708    /// [`message`]: enum.ErrorVariant.html#variant.CustomError.field.message
709    /// # Examples
710    ///
711    /// ```
712    /// # use pest::error::ErrorVariant;
713    /// let variant = ErrorVariant::<()>::CustomError {
714    ///     message: String::from("unexpected error")
715    /// };
716    ///
717    /// println!("{}", variant.message());
718    pub fn message(&self) -> Cow<'_, str> {
719        match self {
720            ErrorVariant::ParsingError {
721                ref positives,
722                ref negatives,
723            } => Cow::Owned(Error::parsing_error_message(positives, negatives, |r| {
724                format!("{:?}", r)
725            })),
726            ErrorVariant::CustomError { ref message } => Cow::Borrowed(message),
727        }
728    }
729}
730
731impl<R: RuleType> fmt::Display for Error<R> {
732    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
733        write!(f, "{}", self.format())
734    }
735}
736
737impl<R: RuleType> fmt::Display for ErrorVariant<R> {
738    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739        match self {
740            ErrorVariant::ParsingError { .. } => write!(f, "parsing error: {}", self.message()),
741            ErrorVariant::CustomError { .. } => write!(f, "{}", self.message()),
742        }
743    }
744}
745
746fn visualize_whitespace(input: &str) -> String {
747    input.to_owned().replace('\r', "␍").replace('\n', "␊")
748}
749
750#[cfg(feature = "miette-error")]
751mod miette_adapter {
752    use alloc::string::ToString;
753    use core::fmt;
754    use std::boxed::Box;
755
756    use crate::error::LineColLocation;
757
758    use super::{Error, RuleType};
759
760    use miette::{Diagnostic, LabeledSpan, SourceCode};
761
762    #[derive(Debug)]
763    pub(crate) struct MietteAdapter<R: RuleType>(pub(crate) Error<R>);
764
765    impl<R: RuleType> Diagnostic for MietteAdapter<R> {
766        fn source_code(&self) -> Option<&dyn SourceCode> {
767            Some(&self.0.inner.line)
768        }
769
770        fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan>>> {
771            let message = self.0.variant.message().to_string();
772
773            let (offset, length) = match self.0.line_col {
774                LineColLocation::Pos((_, c)) => (c - 1, 1),
775                LineColLocation::Span((_, start_c), (_, end_c)) => {
776                    (start_c - 1, end_c - start_c + 1)
777                }
778            };
779
780            let span = LabeledSpan::new(Some(message), offset, length);
781
782            Some(Box::new(std::iter::once(span)))
783        }
784
785        fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
786            Some(Box::new(self.0.message()))
787        }
788    }
789
790    impl<R: RuleType> fmt::Display for MietteAdapter<R> {
791        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792            write!(f, "Failure to parse at {:?}", self.0.line_col)
793        }
794    }
795
796    impl<R> core::error::Error for MietteAdapter<R>
797    where
798        R: RuleType,
799        Self: fmt::Debug + fmt::Display,
800    {
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use alloc::vec;
808
809    #[test]
810    fn display_parsing_error_mixed() {
811        let input = "ab\ncd\nef";
812        let pos = Position::new(input, 4).unwrap();
813        let error: Error<u32> = Error::new_from_pos(
814            ErrorVariant::ParsingError {
815                positives: vec![1, 2, 3],
816                negatives: vec![4, 5, 6],
817            },
818            pos,
819        );
820
821        assert_eq!(
822            format!("{}", error),
823            [
824                " --> 2:2",
825                "  |",
826                "2 | cd",
827                "  |  ^---",
828                "  |",
829                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
830            ]
831            .join("\n")
832        );
833    }
834
835    #[test]
836    fn display_parsing_error_positives() {
837        let input = "ab\ncd\nef";
838        let pos = Position::new(input, 4).unwrap();
839        let error: Error<u32> = Error::new_from_pos(
840            ErrorVariant::ParsingError {
841                positives: vec![1, 2],
842                negatives: vec![],
843            },
844            pos,
845        );
846
847        assert_eq!(
848            format!("{}", error),
849            [
850                " --> 2:2",
851                "  |",
852                "2 | cd",
853                "  |  ^---",
854                "  |",
855                "  = expected 1 or 2"
856            ]
857            .join("\n")
858        );
859    }
860
861    #[test]
862    fn display_parsing_error_negatives() {
863        let input = "ab\ncd\nef";
864        let pos = Position::new(input, 4).unwrap();
865        let error: Error<u32> = Error::new_from_pos(
866            ErrorVariant::ParsingError {
867                positives: vec![],
868                negatives: vec![4, 5, 6],
869            },
870            pos,
871        );
872
873        assert_eq!(
874            format!("{}", error),
875            [
876                " --> 2:2",
877                "  |",
878                "2 | cd",
879                "  |  ^---",
880                "  |",
881                "  = unexpected 4, 5, or 6"
882            ]
883            .join("\n")
884        );
885    }
886
887    #[test]
888    fn display_parsing_error_unknown() {
889        let input = "ab\ncd\nef";
890        let pos = Position::new(input, 4).unwrap();
891        let error: Error<u32> = Error::new_from_pos(
892            ErrorVariant::ParsingError {
893                positives: vec![],
894                negatives: vec![],
895            },
896            pos,
897        );
898
899        assert_eq!(
900            format!("{}", error),
901            [
902                " --> 2:2",
903                "  |",
904                "2 | cd",
905                "  |  ^---",
906                "  |",
907                "  = unknown parsing error"
908            ]
909            .join("\n")
910        );
911    }
912
913    #[test]
914    fn display_custom_pos() {
915        let input = "ab\ncd\nef";
916        let pos = Position::new(input, 4).unwrap();
917        let error: Error<u32> = Error::new_from_pos(
918            ErrorVariant::CustomError {
919                message: "error: big one".to_owned(),
920            },
921            pos,
922        );
923
924        assert_eq!(
925            format!("{}", error),
926            [
927                " --> 2:2",
928                "  |",
929                "2 | cd",
930                "  |  ^---",
931                "  |",
932                "  = error: big one"
933            ]
934            .join("\n")
935        );
936    }
937
938    #[test]
939    fn display_custom_span_two_lines() {
940        let input = "ab\ncd\nefgh";
941        let start = Position::new(input, 4).unwrap();
942        let end = Position::new(input, 9).unwrap();
943        let error: Error<u32> = Error::new_from_span(
944            ErrorVariant::CustomError {
945                message: "error: big one".to_owned(),
946            },
947            start.span(&end),
948        );
949
950        assert_eq!(
951            format!("{}", error),
952            [
953                " --> 2:2",
954                "  |",
955                "2 | cd",
956                "3 | efgh",
957                "  |  ^^",
958                "  |",
959                "  = error: big one"
960            ]
961            .join("\n")
962        );
963    }
964
965    #[test]
966    fn display_custom_span_three_lines() {
967        let input = "ab\ncd\nefgh";
968        let start = Position::new(input, 1).unwrap();
969        let end = Position::new(input, 9).unwrap();
970        let error: Error<u32> = Error::new_from_span(
971            ErrorVariant::CustomError {
972                message: "error: big one".to_owned(),
973            },
974            start.span(&end),
975        );
976
977        assert_eq!(
978            format!("{}", error),
979            [
980                " --> 1:2",
981                "  |",
982                "1 | ab",
983                "  | ...",
984                "3 | efgh",
985                "  |  ^^",
986                "  |",
987                "  = error: big one"
988            ]
989            .join("\n")
990        );
991    }
992
993    #[test]
994    fn display_custom_span_two_lines_inverted_cols() {
995        let input = "abcdef\ngh";
996        let start = Position::new(input, 5).unwrap();
997        let end = Position::new(input, 8).unwrap();
998        let error: Error<u32> = Error::new_from_span(
999            ErrorVariant::CustomError {
1000                message: "error: big one".to_owned(),
1001            },
1002            start.span(&end),
1003        );
1004
1005        assert_eq!(
1006            format!("{}", error),
1007            [
1008                " --> 1:6",
1009                "  |",
1010                "1 | abcdef",
1011                "2 | gh",
1012                "  | ^----^",
1013                "  |",
1014                "  = error: big one"
1015            ]
1016            .join("\n")
1017        );
1018    }
1019
1020    #[test]
1021    fn display_custom_span_end_after_newline() {
1022        let input = "abcdef\n";
1023        let start = Position::new(input, 0).unwrap();
1024        let end = Position::new(input, 7).unwrap();
1025        assert!(start.at_start());
1026        assert!(end.at_end());
1027
1028        let error: Error<u32> = Error::new_from_span(
1029            ErrorVariant::CustomError {
1030                message: "error: big one".to_owned(),
1031            },
1032            start.span(&end),
1033        );
1034
1035        assert_eq!(
1036            format!("{}", error),
1037            [
1038                " --> 1:1",
1039                "  |",
1040                "1 | abcdef␊",
1041                "  | ^-----^",
1042                "  |",
1043                "  = error: big one"
1044            ]
1045            .join("\n")
1046        );
1047    }
1048
1049    #[test]
1050    fn display_custom_span_empty() {
1051        let input = "";
1052        let start = Position::new(input, 0).unwrap();
1053        let end = Position::new(input, 0).unwrap();
1054        assert!(start.at_start());
1055        assert!(end.at_end());
1056
1057        let error: Error<u32> = Error::new_from_span(
1058            ErrorVariant::CustomError {
1059                message: "error: empty".to_owned(),
1060            },
1061            start.span(&end),
1062        );
1063
1064        assert_eq!(
1065            format!("{}", error),
1066            [
1067                " --> 1:1",
1068                "  |",
1069                "1 | ",
1070                "  | ^",
1071                "  |",
1072                "  = error: empty"
1073            ]
1074            .join("\n")
1075        );
1076    }
1077
1078    #[test]
1079    fn mapped_parsing_error() {
1080        let input = "ab\ncd\nef";
1081        let pos = Position::new(input, 4).unwrap();
1082        let error: Error<u32> = Error::new_from_pos(
1083            ErrorVariant::ParsingError {
1084                positives: vec![1, 2, 3],
1085                negatives: vec![4, 5, 6],
1086            },
1087            pos,
1088        )
1089        .renamed_rules(|n| format!("{}", n + 1));
1090
1091        assert_eq!(
1092            format!("{}", error),
1093            [
1094                " --> 2:2",
1095                "  |",
1096                "2 | cd",
1097                "  |  ^---",
1098                "  |",
1099                "  = unexpected 5, 6, or 7; expected 2, 3, or 4"
1100            ]
1101            .join("\n")
1102        );
1103    }
1104
1105    #[test]
1106    fn error_with_path() {
1107        let input = "ab\ncd\nef";
1108        let pos = Position::new(input, 4).unwrap();
1109        let error: Error<u32> = Error::new_from_pos(
1110            ErrorVariant::ParsingError {
1111                positives: vec![1, 2, 3],
1112                negatives: vec![4, 5, 6],
1113            },
1114            pos,
1115        )
1116        .with_path("file.rs");
1117
1118        assert_eq!(
1119            format!("{}", error),
1120            [
1121                " --> file.rs:2:2",
1122                "  |",
1123                "2 | cd",
1124                "  |  ^---",
1125                "  |",
1126                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
1127            ]
1128            .join("\n")
1129        );
1130    }
1131
1132    #[test]
1133    fn underline_with_tabs() {
1134        let input = "a\txbc";
1135        let pos = Position::new(input, 2).unwrap();
1136        let error: Error<u32> = Error::new_from_pos(
1137            ErrorVariant::ParsingError {
1138                positives: vec![1, 2, 3],
1139                negatives: vec![4, 5, 6],
1140            },
1141            pos,
1142        )
1143        .with_path("file.rs");
1144
1145        assert_eq!(
1146            format!("{}", error),
1147            [
1148                " --> file.rs:1:3",
1149                "  |",
1150                "1 | a	xbc",
1151                "  |  	^---",
1152                "  |",
1153                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
1154            ]
1155            .join("\n")
1156        );
1157    }
1158
1159    #[test]
1160    fn pos_to_lcl_conversion() {
1161        let input = "input";
1162
1163        let pos = Position::new(input, 2).unwrap();
1164
1165        assert_eq!(LineColLocation::Pos(pos.line_col()), pos.into());
1166    }
1167
1168    #[test]
1169    fn span_to_lcl_conversion() {
1170        let input = "input";
1171
1172        let span = Span::new(input, 2, 4).unwrap();
1173        let (start, end) = span.split();
1174
1175        assert_eq!(
1176            LineColLocation::Span(start.line_col(), end.line_col()),
1177            span.into()
1178        );
1179    }
1180
1181    #[cfg(feature = "miette-error")]
1182    #[test]
1183    fn miette_error() {
1184        let input = "abc\ndef";
1185        let pos = Position::new(input, 4).unwrap();
1186        let error: Error<u32> = Error::new_from_pos(
1187            ErrorVariant::ParsingError {
1188                positives: vec![1, 2, 3],
1189                negatives: vec![4, 5, 6],
1190            },
1191            pos,
1192        );
1193
1194        let miette_error = miette::Error::new(error.into_miette());
1195
1196        assert_eq!(
1197            format!("{:?}", miette_error),
1198            [
1199                "  \u{1b}[31m×\u{1b}[0m Failure to parse at Pos((2, 1))",
1200                "   ╭────",
1201                " \u{1b}[2m1\u{1b}[0m │ def",
1202                "   · \u{1b}[35;1m┬\u{1b}[0m",
1203                "   · \u{1b}[35;1m╰── \u{1b}[35;1munexpected 4, 5, or 6; expected 1, 2, or 3\u{1b}[0m\u{1b}[0m",
1204                "   ╰────",
1205                "\u{1b}[36m  help: \u{1b}[0munexpected 4, 5, or 6; expected 1, 2, or 3\n"
1206            ]
1207            .join("\n")
1208        );
1209    }
1210}