Skip to main content

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        #[allow(clippy::question_mark)]
434        let attempts = if let Some(ref parse_attempts) = self.inner.parse_attempts {
435            parse_attempts.clone()
436        } else {
437            return None;
438        };
439
440        let spacing = self.spacing() + "   ";
441        let error_position = attempts.max_position;
442        let message = {
443            let mut help_lines: Vec<String> = Vec::new();
444            help_lines.push(String::from("error: parsing error occurred."));
445
446            // Note: at least one of `(un)expected_tokens` must not be empty.
447            for tokens_helper_message in attempts.tokens_helper_messages(is_whitespace, &spacing) {
448                help_lines.push(tokens_helper_message);
449            }
450
451            let call_stacks = attempts.call_stacks();
452            // Group call stacks by their parents so that we can print common header and
453            // several sub helper messages.
454            let mut call_stacks_parents_groups: BTreeMap<Option<R>, Vec<RulesCallStack<R>>> =
455                BTreeMap::new();
456            for call_stack in call_stacks {
457                call_stacks_parents_groups
458                    .entry(call_stack.parent)
459                    .or_default()
460                    .push(call_stack);
461            }
462
463            for (group_parent, group) in call_stacks_parents_groups {
464                if let Some(parent_rule) = group_parent {
465                    let mut contains_meaningful_info = false;
466                    help_lines.push(format!(
467                        "{spacing}help: {}",
468                        if let Some(message) = rule_to_message(&parent_rule) {
469                            contains_meaningful_info = true;
470                            message
471                        } else {
472                            String::from("[Unknown parent rule]")
473                        }
474                    ));
475                    for call_stack in group {
476                        if let Some(r) = call_stack.deepest.get_rule() {
477                            if let Some(message) = rule_to_message(r) {
478                                contains_meaningful_info = true;
479                                help_lines.push(format!("{spacing}      - {message}"));
480                            }
481                        }
482                    }
483                    if !contains_meaningful_info {
484                        // Have to remove useless line for unknown parent rule.
485                        help_lines.pop();
486                    }
487                } else {
488                    for call_stack in group {
489                        // Note that `deepest` rule may be `None`. E.g. in case it corresponds
490                        // to WHITESPACE expected token which has no parent rule (on the top level
491                        // parsing).
492                        if let Some(r) = call_stack.deepest.get_rule() {
493                            let helper_message = rule_to_message(r);
494                            if let Some(helper_message) = helper_message {
495                                help_lines.push(format!("{spacing}help: {helper_message}"));
496                            }
497                        }
498                    }
499                }
500            }
501
502            help_lines.join("\n")
503        };
504        let error = Error::new_from_pos(
505            ErrorVariant::CustomError { message },
506            Position::new_internal(input, error_position),
507        );
508        Some(error)
509    }
510
511    fn start(&self) -> (usize, usize) {
512        match self.line_col {
513            LineColLocation::Pos(line_col) => line_col,
514            LineColLocation::Span(start_line_col, _) => start_line_col,
515        }
516    }
517
518    fn spacing(&self) -> String {
519        let line = match self.line_col {
520            LineColLocation::Pos((line, _)) => line,
521            LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line),
522        };
523
524        let line_str_len = format!("{line}").len();
525
526        let mut spacing = String::new();
527        for _ in 0..line_str_len {
528            spacing.push(' ');
529        }
530
531        spacing
532    }
533
534    fn underline(&self) -> String {
535        let mut underline = String::new();
536
537        let mut start = self.start().1;
538        let end = match self.line_col {
539            LineColLocation::Span(_, (_, mut end)) => {
540                let inverted_cols = start > end;
541                if inverted_cols {
542                    mem::swap(&mut start, &mut end);
543                    start -= 1;
544                    end += 1;
545                }
546
547                Some(end)
548            }
549            _ => None,
550        };
551        let offset = start - 1;
552        let line_chars = self.inner.line.chars();
553
554        for c in line_chars.take(offset) {
555            match c {
556                '\t' => underline.push('\t'),
557                _ => underline.push(' '),
558            }
559        }
560
561        if let Some(end) = end {
562            underline.push('^');
563            if end - start > 1 {
564                for _ in 2..(end - start) {
565                    underline.push('-');
566                }
567                underline.push('^');
568            }
569        } else {
570            underline.push_str("^---")
571        }
572
573        underline
574    }
575
576    fn message(&self) -> String {
577        self.variant.message().to_string()
578    }
579
580    fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String
581    where
582        F: FnMut(&R) -> String,
583    {
584        match (negatives.is_empty(), positives.is_empty()) {
585            (false, false) => format!(
586                "unexpected {}; expected {}",
587                Error::enumerate(negatives, &mut f),
588                Error::enumerate(positives, &mut f)
589            ),
590            (false, true) => format!("unexpected {}", Error::enumerate(negatives, &mut f)),
591            (true, false) => format!("expected {}", Error::enumerate(positives, &mut f)),
592            (true, true) => "unknown parsing error".to_owned(),
593        }
594    }
595
596    fn enumerate<F>(rules: &[R], f: &mut F) -> String
597    where
598        F: FnMut(&R) -> String,
599    {
600        match rules.len() {
601            1 => f(&rules[0]),
602            2 => format!("{} or {}", f(&rules[0]), f(&rules[1])),
603            l => {
604                let non_separated = f(&rules[l - 1]);
605                let separated = rules
606                    .iter()
607                    .take(l - 1)
608                    .map(f)
609                    .collect::<Vec<_>>()
610                    .join(", ");
611                format!("{separated}, or {non_separated}")
612            }
613        }
614    }
615
616    pub(crate) fn format(&self) -> String {
617        let spacing = self.spacing();
618        let path = self
619            .inner
620            .path
621            .as_ref()
622            .map(|path| format!("{path}:"))
623            .unwrap_or_default();
624
625        let pair = (self.line_col.clone(), &self.inner.continued_line);
626        if let (LineColLocation::Span(_, end), Some(ref continued_line)) = pair {
627            let has_line_gap = end.0 - self.start().0 > 1;
628            if has_line_gap {
629                format!(
630                    "{s    }--> {p}{ls}:{c}\n\
631                     {s    } |\n\
632                     {ls:w$} | {line}\n\
633                     {s    } | ...\n\
634                     {le:w$} | {continued_line}\n\
635                     {s    } | {underline}\n\
636                     {s    } |\n\
637                     {s    } = {message}",
638                    s = spacing,
639                    w = spacing.len(),
640                    p = path,
641                    ls = self.start().0,
642                    le = end.0,
643                    c = self.start().1,
644                    line = self.inner.line,
645                    continued_line = continued_line,
646                    underline = self.underline(),
647                    message = self.message()
648                )
649            } else {
650                format!(
651                    "{s    }--> {p}{ls}:{c}\n\
652                     {s    } |\n\
653                     {ls:w$} | {line}\n\
654                     {le:w$} | {continued_line}\n\
655                     {s    } | {underline}\n\
656                     {s    } |\n\
657                     {s    } = {message}",
658                    s = spacing,
659                    w = spacing.len(),
660                    p = path,
661                    ls = self.start().0,
662                    le = end.0,
663                    c = self.start().1,
664                    line = self.inner.line,
665                    continued_line = continued_line,
666                    underline = self.underline(),
667                    message = self.message()
668                )
669            }
670        } else {
671            format!(
672                "{s}--> {p}{l}:{c}\n\
673                 {s} |\n\
674                 {l} | {line}\n\
675                 {s} | {underline}\n\
676                 {s} |\n\
677                 {s} = {message}",
678                s = spacing,
679                p = path,
680                l = self.start().0,
681                c = self.start().1,
682                line = self.inner.line,
683                underline = self.underline(),
684                message = self.message()
685            )
686        }
687    }
688
689    #[cfg(feature = "miette-error")]
690    /// Turns an error into a [miette](crates.io/miette) Diagnostic.
691    pub fn into_miette(self) -> impl ::miette::Diagnostic {
692        miette_adapter::MietteAdapter(self)
693    }
694}
695
696impl<R: RuleType> ErrorVariant<R> {
697    ///
698    /// Returns the error message for [`ErrorVariant`]
699    ///
700    /// If [`ErrorVariant`] is [`CustomError`], it returns a
701    /// [`Cow::Borrowed`] reference to [`message`]. If [`ErrorVariant`] is [`ParsingError`], a
702    /// [`Cow::Owned`] containing "expected [ErrorVariant::ParsingError::positives] [ErrorVariant::ParsingError::negatives]" is returned.
703    ///
704    /// [`ErrorVariant`]: enum.ErrorVariant.html
705    /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError
706    /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError
707    /// [`Cow::Owned`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Owned
708    /// [`Cow::Borrowed`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Borrowed
709    /// [`message`]: enum.ErrorVariant.html#variant.CustomError.field.message
710    /// # Examples
711    ///
712    /// ```
713    /// # use pest::error::ErrorVariant;
714    /// let variant = ErrorVariant::<()>::CustomError {
715    ///     message: String::from("unexpected error")
716    /// };
717    ///
718    /// println!("{}", variant.message());
719    pub fn message(&self) -> Cow<'_, str> {
720        match self {
721            ErrorVariant::ParsingError {
722                ref positives,
723                ref negatives,
724            } => Cow::Owned(Error::parsing_error_message(positives, negatives, |r| {
725                format!("{r:?}")
726            })),
727            ErrorVariant::CustomError { ref message } => Cow::Borrowed(message),
728        }
729    }
730}
731
732impl<R: RuleType> fmt::Display for Error<R> {
733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734        write!(f, "{}", self.format())
735    }
736}
737
738impl<R: RuleType> fmt::Display for ErrorVariant<R> {
739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740        match self {
741            ErrorVariant::ParsingError { .. } => write!(f, "parsing error: {}", self.message()),
742            ErrorVariant::CustomError { .. } => write!(f, "{}", self.message()),
743        }
744    }
745}
746
747fn visualize_whitespace(input: &str) -> String {
748    input.to_owned().replace('\r', "␍").replace('\n', "␊")
749}
750
751#[cfg(feature = "miette-error")]
752mod miette_adapter {
753    use alloc::string::ToString;
754    use core::fmt;
755    use std::boxed::Box;
756
757    use crate::error::LineColLocation;
758
759    use super::{Error, RuleType};
760
761    use miette::{Diagnostic, LabeledSpan, SourceCode};
762
763    #[derive(Debug)]
764    pub(crate) struct MietteAdapter<R: RuleType>(pub(crate) Error<R>);
765
766    impl<R: RuleType> Diagnostic for MietteAdapter<R> {
767        fn source_code(&self) -> Option<&dyn SourceCode> {
768            Some(&self.0.inner.line)
769        }
770
771        fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan>>> {
772            let message = self.0.variant.message().to_string();
773
774            let (offset, length) = match self.0.line_col {
775                LineColLocation::Pos((_, c)) => (c - 1, 1),
776                LineColLocation::Span((_, start_c), (_, end_c)) => {
777                    (start_c - 1, end_c - start_c + 1)
778                }
779            };
780
781            let span = LabeledSpan::new(Some(message), offset, length);
782
783            Some(Box::new(std::iter::once(span)))
784        }
785
786        fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
787            Some(Box::new(self.0.message()))
788        }
789    }
790
791    impl<R: RuleType> fmt::Display for MietteAdapter<R> {
792        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
793            write!(f, "Failure to parse at {:?}", self.0.line_col)
794        }
795    }
796
797    impl<R> core::error::Error for MietteAdapter<R>
798    where
799        R: RuleType,
800        Self: fmt::Debug + fmt::Display,
801    {
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use alloc::vec;
809
810    #[test]
811    fn display_parsing_error_mixed() {
812        let input = "ab\ncd\nef";
813        let pos = Position::new(input, 4).unwrap();
814        let error: Error<u32> = Error::new_from_pos(
815            ErrorVariant::ParsingError {
816                positives: vec![1, 2, 3],
817                negatives: vec![4, 5, 6],
818            },
819            pos,
820        );
821
822        assert_eq!(
823            format!("{error}"),
824            [
825                " --> 2:2",
826                "  |",
827                "2 | cd",
828                "  |  ^---",
829                "  |",
830                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
831            ]
832            .join("\n")
833        );
834    }
835
836    #[test]
837    fn display_parsing_error_positives() {
838        let input = "ab\ncd\nef";
839        let pos = Position::new(input, 4).unwrap();
840        let error: Error<u32> = Error::new_from_pos(
841            ErrorVariant::ParsingError {
842                positives: vec![1, 2],
843                negatives: vec![],
844            },
845            pos,
846        );
847
848        assert_eq!(
849            format!("{error}"),
850            [
851                " --> 2:2",
852                "  |",
853                "2 | cd",
854                "  |  ^---",
855                "  |",
856                "  = expected 1 or 2"
857            ]
858            .join("\n")
859        );
860    }
861
862    #[test]
863    fn display_parsing_error_negatives() {
864        let input = "ab\ncd\nef";
865        let pos = Position::new(input, 4).unwrap();
866        let error: Error<u32> = Error::new_from_pos(
867            ErrorVariant::ParsingError {
868                positives: vec![],
869                negatives: vec![4, 5, 6],
870            },
871            pos,
872        );
873
874        assert_eq!(
875            format!("{error}"),
876            [
877                " --> 2:2",
878                "  |",
879                "2 | cd",
880                "  |  ^---",
881                "  |",
882                "  = unexpected 4, 5, or 6"
883            ]
884            .join("\n")
885        );
886    }
887
888    #[test]
889    fn display_parsing_error_unknown() {
890        let input = "ab\ncd\nef";
891        let pos = Position::new(input, 4).unwrap();
892        let error: Error<u32> = Error::new_from_pos(
893            ErrorVariant::ParsingError {
894                positives: vec![],
895                negatives: vec![],
896            },
897            pos,
898        );
899
900        assert_eq!(
901            format!("{error}"),
902            [
903                " --> 2:2",
904                "  |",
905                "2 | cd",
906                "  |  ^---",
907                "  |",
908                "  = unknown parsing error"
909            ]
910            .join("\n")
911        );
912    }
913
914    #[test]
915    fn display_custom_pos() {
916        let input = "ab\ncd\nef";
917        let pos = Position::new(input, 4).unwrap();
918        let error: Error<u32> = Error::new_from_pos(
919            ErrorVariant::CustomError {
920                message: "error: big one".to_owned(),
921            },
922            pos,
923        );
924
925        assert_eq!(
926            format!("{error}"),
927            [
928                " --> 2:2",
929                "  |",
930                "2 | cd",
931                "  |  ^---",
932                "  |",
933                "  = error: big one"
934            ]
935            .join("\n")
936        );
937    }
938
939    #[test]
940    fn display_custom_span_two_lines() {
941        let input = "ab\ncd\nefgh";
942        let start = Position::new(input, 4).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                " --> 2:2",
955                "  |",
956                "2 | cd",
957                "3 | efgh",
958                "  |  ^^",
959                "  |",
960                "  = error: big one"
961            ]
962            .join("\n")
963        );
964    }
965
966    #[test]
967    fn display_custom_span_three_lines() {
968        let input = "ab\ncd\nefgh";
969        let start = Position::new(input, 1).unwrap();
970        let end = Position::new(input, 9).unwrap();
971        let error: Error<u32> = Error::new_from_span(
972            ErrorVariant::CustomError {
973                message: "error: big one".to_owned(),
974            },
975            start.span(&end),
976        );
977
978        assert_eq!(
979            format!("{error}"),
980            [
981                " --> 1:2",
982                "  |",
983                "1 | ab",
984                "  | ...",
985                "3 | efgh",
986                "  |  ^^",
987                "  |",
988                "  = error: big one"
989            ]
990            .join("\n")
991        );
992    }
993
994    #[test]
995    fn display_custom_span_two_lines_inverted_cols() {
996        let input = "abcdef\ngh";
997        let start = Position::new(input, 5).unwrap();
998        let end = Position::new(input, 8).unwrap();
999        let error: Error<u32> = Error::new_from_span(
1000            ErrorVariant::CustomError {
1001                message: "error: big one".to_owned(),
1002            },
1003            start.span(&end),
1004        );
1005
1006        assert_eq!(
1007            format!("{error}"),
1008            [
1009                " --> 1:6",
1010                "  |",
1011                "1 | abcdef",
1012                "2 | gh",
1013                "  | ^----^",
1014                "  |",
1015                "  = error: big one"
1016            ]
1017            .join("\n")
1018        );
1019    }
1020
1021    #[test]
1022    fn display_custom_span_end_after_newline() {
1023        let input = "abcdef\n";
1024        let start = Position::new(input, 0).unwrap();
1025        let end = Position::new(input, 7).unwrap();
1026        assert!(start.at_start());
1027        assert!(end.at_end());
1028
1029        let error: Error<u32> = Error::new_from_span(
1030            ErrorVariant::CustomError {
1031                message: "error: big one".to_owned(),
1032            },
1033            start.span(&end),
1034        );
1035
1036        assert_eq!(
1037            format!("{error}"),
1038            [
1039                " --> 1:1",
1040                "  |",
1041                "1 | abcdef␊",
1042                "  | ^-----^",
1043                "  |",
1044                "  = error: big one"
1045            ]
1046            .join("\n")
1047        );
1048    }
1049
1050    #[test]
1051    fn display_custom_span_end_after_inner_newline() {
1052        let input = "abcdef\nghi";
1053        let start = Position::new(input, 0).unwrap();
1054        let end = Position::new(input, 7).unwrap();
1055        assert!(start.at_start());
1056        assert!(!end.at_end());
1057
1058        let error: Error<u32> = Error::new_from_span(
1059            ErrorVariant::CustomError {
1060                message: "error: big one".to_owned(),
1061            },
1062            start.span(&end),
1063        );
1064
1065        assert_eq!(
1066            format!("{error}"),
1067            [
1068                " --> 1:1",
1069                "  |",
1070                "1 | abcdef␊",
1071                "  | ^-----^",
1072                "  |",
1073                "  = error: big one"
1074            ]
1075            .join("\n")
1076        );
1077    }
1078
1079    #[test]
1080    fn display_custom_span_empty() {
1081        let input = "";
1082        let start = Position::new(input, 0).unwrap();
1083        let end = Position::new(input, 0).unwrap();
1084        assert!(start.at_start());
1085        assert!(end.at_end());
1086
1087        let error: Error<u32> = Error::new_from_span(
1088            ErrorVariant::CustomError {
1089                message: "error: empty".to_owned(),
1090            },
1091            start.span(&end),
1092        );
1093
1094        assert_eq!(
1095            format!("{error}"),
1096            [
1097                " --> 1:1",
1098                "  |",
1099                "1 | ",
1100                "  | ^",
1101                "  |",
1102                "  = error: empty"
1103            ]
1104            .join("\n")
1105        );
1106    }
1107
1108    #[test]
1109    fn mapped_parsing_error() {
1110        let input = "ab\ncd\nef";
1111        let pos = Position::new(input, 4).unwrap();
1112        let error: Error<u32> = Error::new_from_pos(
1113            ErrorVariant::ParsingError {
1114                positives: vec![1, 2, 3],
1115                negatives: vec![4, 5, 6],
1116            },
1117            pos,
1118        )
1119        .renamed_rules(|n| format!("{}", n + 1));
1120
1121        assert_eq!(
1122            format!("{error}"),
1123            [
1124                " --> 2:2",
1125                "  |",
1126                "2 | cd",
1127                "  |  ^---",
1128                "  |",
1129                "  = unexpected 5, 6, or 7; expected 2, 3, or 4"
1130            ]
1131            .join("\n")
1132        );
1133    }
1134
1135    #[test]
1136    fn error_with_path() {
1137        let input = "ab\ncd\nef";
1138        let pos = Position::new(input, 4).unwrap();
1139        let error: Error<u32> = Error::new_from_pos(
1140            ErrorVariant::ParsingError {
1141                positives: vec![1, 2, 3],
1142                negatives: vec![4, 5, 6],
1143            },
1144            pos,
1145        )
1146        .with_path("file.rs");
1147
1148        assert_eq!(
1149            format!("{error}"),
1150            [
1151                " --> file.rs:2:2",
1152                "  |",
1153                "2 | cd",
1154                "  |  ^---",
1155                "  |",
1156                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
1157            ]
1158            .join("\n")
1159        );
1160    }
1161
1162    #[test]
1163    fn underline_with_tabs() {
1164        let input = "a\txbc";
1165        let pos = Position::new(input, 2).unwrap();
1166        let error: Error<u32> = Error::new_from_pos(
1167            ErrorVariant::ParsingError {
1168                positives: vec![1, 2, 3],
1169                negatives: vec![4, 5, 6],
1170            },
1171            pos,
1172        )
1173        .with_path("file.rs");
1174
1175        assert_eq!(
1176            format!("{error}"),
1177            [
1178                " --> file.rs:1:3",
1179                "  |",
1180                "1 | a	xbc",
1181                "  |  	^---",
1182                "  |",
1183                "  = unexpected 4, 5, or 6; expected 1, 2, or 3"
1184            ]
1185            .join("\n")
1186        );
1187    }
1188
1189    #[test]
1190    fn pos_to_lcl_conversion() {
1191        let input = "input";
1192
1193        let pos = Position::new(input, 2).unwrap();
1194
1195        assert_eq!(LineColLocation::Pos(pos.line_col()), pos.into());
1196    }
1197
1198    #[test]
1199    fn span_to_lcl_conversion() {
1200        let input = "input";
1201
1202        let span = Span::new(input, 2, 4).unwrap();
1203        let (start, end) = span.split();
1204
1205        assert_eq!(
1206            LineColLocation::Span(start.line_col(), end.line_col()),
1207            span.into()
1208        );
1209    }
1210
1211    #[cfg(feature = "miette-error")]
1212    #[test]
1213    fn miette_error() {
1214        // Force color output so the test passes regardless of TTY availability
1215        let prev_force_color = std::env::var_os("FORCE_COLOR");
1216        std::env::set_var("FORCE_COLOR", "1");
1217
1218        let input = "abc\ndef";
1219        let pos = Position::new(input, 4).unwrap();
1220        let error: Error<u32> = Error::new_from_pos(
1221            ErrorVariant::ParsingError {
1222                positives: vec![1, 2, 3],
1223                negatives: vec![4, 5, 6],
1224            },
1225            pos,
1226        );
1227
1228        let miette_error = miette::Error::new(error.into_miette());
1229
1230        assert_eq!(
1231            format!("{miette_error:?}"),
1232            [
1233                "  \u{1b}[31m×\u{1b}[0m Failure to parse at Pos((2, 1))",
1234                "   ╭────",
1235                " \u{1b}[2m1\u{1b}[0m │ def",
1236                "   · \u{1b}[35;1m┬\u{1b}[0m",
1237                "   · \u{1b}[35;1m╰── \u{1b}[35;1munexpected 4, 5, or 6; expected 1, 2, or 3\u{1b}[0m\u{1b}[0m",
1238                "   ╰────",
1239                "\u{1b}[36m  help: \u{1b}[0munexpected 4, 5, or 6; expected 1, 2, or 3\n"
1240            ]
1241            .join("\n")
1242        );
1243
1244        // Restore previous FORCE_COLOR state to avoid cross-test leakage
1245        if let Some(prev) = prev_force_color {
1246            std::env::set_var("FORCE_COLOR", prev);
1247        } else {
1248            std::env::remove_var("FORCE_COLOR");
1249        }
1250    }
1251}