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