| // pest. The Elegant Parser |
| // Copyright (c) 2018 Dragoș Tiselice |
| // |
| // Licensed under the Apache License, Version 2.0 |
| // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT |
| // license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| // option. All files in the project carrying such notice may not be copied, |
| // modified, or distributed except according to those terms. |
| |
| //! Types for different kinds of parsing failures. |
| |
| use crate::parser_state::{ParseAttempts, ParsingToken, RulesCallStack}; |
| use alloc::borrow::Cow; |
| use alloc::borrow::ToOwned; |
| use alloc::boxed::Box; |
| use alloc::collections::{BTreeMap, BTreeSet}; |
| use alloc::format; |
| use alloc::string::String; |
| use alloc::string::ToString; |
| use alloc::vec; |
| use alloc::vec::Vec; |
| use core::cmp; |
| use core::fmt; |
| use core::mem; |
| |
| use crate::position::Position; |
| use crate::span::Span; |
| use crate::RuleType; |
| |
| /// Parse-related error type. |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| #[cfg_attr(feature = "std", derive(thiserror::Error))] |
| pub struct Error<R> { |
| /// Variant of the error |
| pub variant: ErrorVariant<R>, |
| /// Location within the input string |
| pub location: InputLocation, |
| /// Line/column within the input string |
| pub line_col: LineColLocation, |
| path: Option<String>, |
| line: String, |
| continued_line: Option<String>, |
| parse_attempts: Option<ParseAttempts<R>>, |
| } |
| |
| /// Different kinds of parsing errors. |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| #[cfg_attr(feature = "std", derive(thiserror::Error))] |
| pub enum ErrorVariant<R> { |
| /// Generated parsing error with expected and unexpected `Rule`s |
| ParsingError { |
| /// Positive attempts |
| positives: Vec<R>, |
| /// Negative attempts |
| negatives: Vec<R>, |
| }, |
| /// Custom error with a message |
| CustomError { |
| /// Short explanation |
| message: String, |
| }, |
| } |
| |
| /// Where an `Error` has occurred. |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| pub enum InputLocation { |
| /// `Error` was created by `Error::new_from_pos` |
| Pos(usize), |
| /// `Error` was created by `Error::new_from_span` |
| Span((usize, usize)), |
| } |
| |
| /// Line/column where an `Error` has occurred. |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| pub enum LineColLocation { |
| /// Line/column pair if `Error` was created by `Error::new_from_pos` |
| Pos((usize, usize)), |
| /// Line/column pairs if `Error` was created by `Error::new_from_span` |
| Span((usize, usize), (usize, usize)), |
| } |
| |
| impl From<Position<'_>> for LineColLocation { |
| fn from(value: Position<'_>) -> Self { |
| Self::Pos(value.line_col()) |
| } |
| } |
| |
| impl From<Span<'_>> for LineColLocation { |
| fn from(value: Span<'_>) -> Self { |
| let (start, end) = value.split(); |
| Self::Span(start.line_col(), end.line_col()) |
| } |
| } |
| |
| /// Function mapping rule to its helper message defined by user. |
| pub type RuleToMessageFn<R> = Box<dyn Fn(&R) -> Option<String>>; |
| /// Function mapping string element to bool denoting whether it's a whitespace defined by user. |
| pub type IsWhitespaceFn = Box<dyn Fn(String) -> bool>; |
| |
| impl ParsingToken { |
| pub fn is_whitespace(&self, is_whitespace: &IsWhitespaceFn) -> bool { |
| match self { |
| ParsingToken::Sensitive { token } => is_whitespace(token.clone()), |
| ParsingToken::Insensitive { token } => is_whitespace(token.clone()), |
| ParsingToken::Range { .. } => false, |
| ParsingToken::BuiltInRule => false, |
| } |
| } |
| } |
| |
| impl<R: RuleType> ParseAttempts<R> { |
| /// Helper formatting function to get message informing about tokens we've |
| /// (un)expected to see. |
| /// Used as a part of `parse_attempts_error`. |
| fn tokens_helper_messages( |
| &self, |
| is_whitespace_fn: &IsWhitespaceFn, |
| spacing: &str, |
| ) -> Vec<String> { |
| let mut helper_messages = Vec::new(); |
| let tokens_header_pairs = vec![ |
| (self.expected_tokens(), "expected"), |
| (self.unexpected_tokens(), "unexpected"), |
| ]; |
| |
| for (tokens, header) in &tokens_header_pairs { |
| if tokens.is_empty() { |
| continue; |
| } |
| |
| let mut helper_tokens_message = format!("{spacing}note: {header} "); |
| helper_tokens_message.push_str(if tokens.len() == 1 { |
| "token: " |
| } else { |
| "one of tokens: " |
| }); |
| |
| let expected_tokens_set: BTreeSet<String> = tokens |
| .iter() |
| .map(|token| { |
| if token.is_whitespace(is_whitespace_fn) { |
| String::from("WHITESPACE") |
| } else { |
| format!("`{}`", token) |
| } |
| }) |
| .collect(); |
| |
| helper_tokens_message.push_str( |
| &expected_tokens_set |
| .iter() |
| .cloned() |
| .collect::<Vec<String>>() |
| .join(", "), |
| ); |
| helper_messages.push(helper_tokens_message); |
| } |
| |
| helper_messages |
| } |
| } |
| |
| impl<R: RuleType> Error<R> { |
| /// Creates `Error` from `ErrorVariant` and `Position`. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::{Error, ErrorVariant}; |
| /// # use pest::Position; |
| /// # #[allow(non_camel_case_types)] |
| /// # #[allow(dead_code)] |
| /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| /// # enum Rule { |
| /// # open_paren, |
| /// # closed_paren |
| /// # } |
| /// # let input = ""; |
| /// # let pos = Position::from_start(input); |
| /// let error = Error::new_from_pos( |
| /// ErrorVariant::ParsingError { |
| /// positives: vec![Rule::open_paren], |
| /// negatives: vec![Rule::closed_paren], |
| /// }, |
| /// pos |
| /// ); |
| /// |
| /// println!("{}", error); |
| /// ``` |
| pub fn new_from_pos(variant: ErrorVariant<R>, pos: Position<'_>) -> Error<R> { |
| let visualize_ws = pos.match_char('\n') || pos.match_char('\r'); |
| let line_of = pos.line_of(); |
| let line = if visualize_ws { |
| visualize_whitespace(line_of) |
| } else { |
| line_of.replace(&['\r', '\n'][..], "") |
| }; |
| Error { |
| variant, |
| location: InputLocation::Pos(pos.pos()), |
| path: None, |
| line, |
| continued_line: None, |
| line_col: LineColLocation::Pos(pos.line_col()), |
| parse_attempts: None, |
| } |
| } |
| |
| /// Wrapper function to track `parse_attempts` as a result |
| /// of `state` function call in `parser_state.rs`. |
| pub(crate) fn new_from_pos_with_parsing_attempts( |
| variant: ErrorVariant<R>, |
| pos: Position<'_>, |
| parse_attempts: ParseAttempts<R>, |
| ) -> Error<R> { |
| let mut error = Self::new_from_pos(variant, pos); |
| error.parse_attempts = Some(parse_attempts); |
| error |
| } |
| |
| /// Creates `Error` from `ErrorVariant` and `Span`. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::{Error, ErrorVariant}; |
| /// # use pest::{Position, Span}; |
| /// # #[allow(non_camel_case_types)] |
| /// # #[allow(dead_code)] |
| /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| /// # enum Rule { |
| /// # open_paren, |
| /// # closed_paren |
| /// # } |
| /// # let input = ""; |
| /// # let start = Position::from_start(input); |
| /// # let end = start.clone(); |
| /// # let span = start.span(&end); |
| /// let error = Error::new_from_span( |
| /// ErrorVariant::ParsingError { |
| /// positives: vec![Rule::open_paren], |
| /// negatives: vec![Rule::closed_paren], |
| /// }, |
| /// span |
| /// ); |
| /// |
| /// println!("{}", error); |
| /// ``` |
| pub fn new_from_span(variant: ErrorVariant<R>, span: Span<'_>) -> Error<R> { |
| let end = span.end_pos(); |
| let mut end_line_col = end.line_col(); |
| // end position is after a \n, so we want to point to the visual lf symbol |
| if end_line_col.1 == 1 { |
| let mut visual_end = end; |
| visual_end.skip_back(1); |
| let lc = visual_end.line_col(); |
| end_line_col = (lc.0, lc.1 + 1); |
| }; |
| |
| let mut line_iter = span.lines(); |
| let sl = line_iter.next().unwrap_or(""); |
| let mut chars = span.as_str().chars(); |
| let visualize_ws = matches!(chars.next(), Some('\n') | Some('\r')) |
| || matches!(chars.last(), Some('\n') | Some('\r')); |
| let start_line = if visualize_ws { |
| visualize_whitespace(sl) |
| } else { |
| sl.to_owned().replace(&['\r', '\n'][..], "") |
| }; |
| let ll = line_iter.last(); |
| let continued_line = if visualize_ws { |
| ll.map(str::to_owned) |
| } else { |
| ll.map(visualize_whitespace) |
| }; |
| |
| Error { |
| variant, |
| location: InputLocation::Span((span.start(), end.pos())), |
| path: None, |
| line: start_line, |
| continued_line, |
| line_col: LineColLocation::Span(span.start_pos().line_col(), end_line_col), |
| parse_attempts: None, |
| } |
| } |
| |
| /// Returns `Error` variant with `path` which is shown when formatted with `Display`. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::{Error, ErrorVariant}; |
| /// # use pest::Position; |
| /// # #[allow(non_camel_case_types)] |
| /// # #[allow(dead_code)] |
| /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| /// # enum Rule { |
| /// # open_paren, |
| /// # closed_paren |
| /// # } |
| /// # let input = ""; |
| /// # let pos = Position::from_start(input); |
| /// Error::new_from_pos( |
| /// ErrorVariant::ParsingError { |
| /// positives: vec![Rule::open_paren], |
| /// negatives: vec![Rule::closed_paren], |
| /// }, |
| /// pos |
| /// ).with_path("file.rs"); |
| /// ``` |
| pub fn with_path(mut self, path: &str) -> Error<R> { |
| self.path = Some(path.to_owned()); |
| |
| self |
| } |
| |
| /// Returns the path set using [`Error::with_path()`]. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::{Error, ErrorVariant}; |
| /// # use pest::Position; |
| /// # #[allow(non_camel_case_types)] |
| /// # #[allow(dead_code)] |
| /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| /// # enum Rule { |
| /// # open_paren, |
| /// # closed_paren |
| /// # } |
| /// # let input = ""; |
| /// # let pos = Position::from_start(input); |
| /// # let error = Error::new_from_pos( |
| /// # ErrorVariant::ParsingError { |
| /// # positives: vec![Rule::open_paren], |
| /// # negatives: vec![Rule::closed_paren], |
| /// # }, |
| /// # pos); |
| /// let error = error.with_path("file.rs"); |
| /// assert_eq!(Some("file.rs"), error.path()); |
| /// ``` |
| pub fn path(&self) -> Option<&str> { |
| self.path.as_deref() |
| } |
| |
| /// Returns the line that the error is on. |
| pub fn line(&self) -> &str { |
| self.line.as_str() |
| } |
| |
| /// Renames all `Rule`s if this is a [`ParsingError`]. It does nothing when called on a |
| /// [`CustomError`]. |
| /// |
| /// Useful in order to rename verbose rules or have detailed per-`Rule` formatting. |
| /// |
| /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError |
| /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::{Error, ErrorVariant}; |
| /// # use pest::Position; |
| /// # #[allow(non_camel_case_types)] |
| /// # #[allow(dead_code)] |
| /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| /// # enum Rule { |
| /// # open_paren, |
| /// # closed_paren |
| /// # } |
| /// # let input = ""; |
| /// # let pos = Position::from_start(input); |
| /// Error::new_from_pos( |
| /// ErrorVariant::ParsingError { |
| /// positives: vec![Rule::open_paren], |
| /// negatives: vec![Rule::closed_paren], |
| /// }, |
| /// pos |
| /// ).renamed_rules(|rule| { |
| /// match *rule { |
| /// Rule::open_paren => "(".to_owned(), |
| /// Rule::closed_paren => "closed paren".to_owned() |
| /// } |
| /// }); |
| /// ``` |
| pub fn renamed_rules<F>(mut self, f: F) -> Error<R> |
| where |
| F: FnMut(&R) -> String, |
| { |
| let variant = match self.variant { |
| ErrorVariant::ParsingError { |
| positives, |
| negatives, |
| } => { |
| let message = Error::parsing_error_message(&positives, &negatives, f); |
| ErrorVariant::CustomError { message } |
| } |
| variant => variant, |
| }; |
| |
| self.variant = variant; |
| |
| self |
| } |
| |
| /// Get detailed information about errored rules sequence. |
| /// Returns `Some(results)` only for `ParsingError`. |
| pub fn parse_attempts(&self) -> Option<ParseAttempts<R>> { |
| self.parse_attempts.clone() |
| } |
| |
| /// Get error message based on parsing attempts. |
| /// Returns `None` in case self `parse_attempts` is `None`. |
| pub fn parse_attempts_error( |
| &self, |
| input: &str, |
| rule_to_message: &RuleToMessageFn<R>, |
| is_whitespace: &IsWhitespaceFn, |
| ) -> Option<Error<R>> { |
| let attempts = if let Some(ref parse_attempts) = self.parse_attempts { |
| parse_attempts.clone() |
| } else { |
| return None; |
| }; |
| |
| let spacing = self.spacing() + " "; |
| let error_position = attempts.max_position; |
| let message = { |
| let mut help_lines: Vec<String> = Vec::new(); |
| help_lines.push(String::from("error: parsing error occurred.")); |
| |
| // Note: at least one of `(un)expected_tokens` must not be empty. |
| for tokens_helper_message in attempts.tokens_helper_messages(is_whitespace, &spacing) { |
| help_lines.push(tokens_helper_message); |
| } |
| |
| let call_stacks = attempts.call_stacks(); |
| // Group call stacks by their parents so that we can print common header and |
| // several sub helper messages. |
| let mut call_stacks_parents_groups: BTreeMap<Option<R>, Vec<RulesCallStack<R>>> = |
| BTreeMap::new(); |
| for call_stack in call_stacks { |
| call_stacks_parents_groups |
| .entry(call_stack.parent) |
| .or_default() |
| .push(call_stack); |
| } |
| |
| for (group_parent, group) in call_stacks_parents_groups { |
| if let Some(parent_rule) = group_parent { |
| let mut contains_meaningful_info = false; |
| help_lines.push(format!( |
| "{spacing}help: {}", |
| if let Some(message) = rule_to_message(&parent_rule) { |
| contains_meaningful_info = true; |
| message |
| } else { |
| String::from("[Unknown parent rule]") |
| } |
| )); |
| for call_stack in group { |
| if let Some(r) = call_stack.deepest.get_rule() { |
| if let Some(message) = rule_to_message(r) { |
| contains_meaningful_info = true; |
| help_lines.push(format!("{spacing} - {message}")); |
| } |
| } |
| } |
| if !contains_meaningful_info { |
| // Have to remove useless line for unknown parent rule. |
| help_lines.pop(); |
| } |
| } else { |
| for call_stack in group { |
| // Note that `deepest` rule may be `None`. E.g. in case it corresponds |
| // to WHITESPACE expected token which has no parent rule (on the top level |
| // parsing). |
| if let Some(r) = call_stack.deepest.get_rule() { |
| let helper_message = rule_to_message(r); |
| if let Some(helper_message) = helper_message { |
| help_lines.push(format!("{spacing}help: {helper_message}")); |
| } |
| } |
| } |
| } |
| } |
| |
| help_lines.join("\n") |
| }; |
| let error = Error::new_from_pos( |
| ErrorVariant::CustomError { message }, |
| Position::new_internal(input, error_position), |
| ); |
| Some(error) |
| } |
| |
| fn start(&self) -> (usize, usize) { |
| match self.line_col { |
| LineColLocation::Pos(line_col) => line_col, |
| LineColLocation::Span(start_line_col, _) => start_line_col, |
| } |
| } |
| |
| fn spacing(&self) -> String { |
| let line = match self.line_col { |
| LineColLocation::Pos((line, _)) => line, |
| LineColLocation::Span((start_line, _), (end_line, _)) => cmp::max(start_line, end_line), |
| }; |
| |
| let line_str_len = format!("{}", line).len(); |
| |
| let mut spacing = String::new(); |
| for _ in 0..line_str_len { |
| spacing.push(' '); |
| } |
| |
| spacing |
| } |
| |
| fn underline(&self) -> String { |
| let mut underline = String::new(); |
| |
| let mut start = self.start().1; |
| let end = match self.line_col { |
| LineColLocation::Span(_, (_, mut end)) => { |
| let inverted_cols = start > end; |
| if inverted_cols { |
| mem::swap(&mut start, &mut end); |
| start -= 1; |
| end += 1; |
| } |
| |
| Some(end) |
| } |
| _ => None, |
| }; |
| let offset = start - 1; |
| let line_chars = self.line.chars(); |
| |
| for c in line_chars.take(offset) { |
| match c { |
| '\t' => underline.push('\t'), |
| _ => underline.push(' '), |
| } |
| } |
| |
| if let Some(end) = end { |
| underline.push('^'); |
| if end - start > 1 { |
| for _ in 2..(end - start) { |
| underline.push('-'); |
| } |
| underline.push('^'); |
| } |
| } else { |
| underline.push_str("^---") |
| } |
| |
| underline |
| } |
| |
| fn message(&self) -> String { |
| self.variant.message().to_string() |
| } |
| |
| fn parsing_error_message<F>(positives: &[R], negatives: &[R], mut f: F) -> String |
| where |
| F: FnMut(&R) -> String, |
| { |
| match (negatives.is_empty(), positives.is_empty()) { |
| (false, false) => format!( |
| "unexpected {}; expected {}", |
| Error::enumerate(negatives, &mut f), |
| Error::enumerate(positives, &mut f) |
| ), |
| (false, true) => format!("unexpected {}", Error::enumerate(negatives, &mut f)), |
| (true, false) => format!("expected {}", Error::enumerate(positives, &mut f)), |
| (true, true) => "unknown parsing error".to_owned(), |
| } |
| } |
| |
| fn enumerate<F>(rules: &[R], f: &mut F) -> String |
| where |
| F: FnMut(&R) -> String, |
| { |
| match rules.len() { |
| 1 => f(&rules[0]), |
| 2 => format!("{} or {}", f(&rules[0]), f(&rules[1])), |
| l => { |
| let non_separated = f(&rules[l - 1]); |
| let separated = rules |
| .iter() |
| .take(l - 1) |
| .map(f) |
| .collect::<Vec<_>>() |
| .join(", "); |
| format!("{}, or {}", separated, non_separated) |
| } |
| } |
| } |
| |
| pub(crate) fn format(&self) -> String { |
| let spacing = self.spacing(); |
| let path = self |
| .path |
| .as_ref() |
| .map(|path| format!("{}:", path)) |
| .unwrap_or_default(); |
| |
| let pair = (self.line_col.clone(), &self.continued_line); |
| if let (LineColLocation::Span(_, end), Some(ref continued_line)) = pair { |
| let has_line_gap = end.0 - self.start().0 > 1; |
| if has_line_gap { |
| format!( |
| "{s }--> {p}{ls}:{c}\n\ |
| {s } |\n\ |
| {ls:w$} | {line}\n\ |
| {s } | ...\n\ |
| {le:w$} | {continued_line}\n\ |
| {s } | {underline}\n\ |
| {s } |\n\ |
| {s } = {message}", |
| s = spacing, |
| w = spacing.len(), |
| p = path, |
| ls = self.start().0, |
| le = end.0, |
| c = self.start().1, |
| line = self.line, |
| continued_line = continued_line, |
| underline = self.underline(), |
| message = self.message() |
| ) |
| } else { |
| format!( |
| "{s }--> {p}{ls}:{c}\n\ |
| {s } |\n\ |
| {ls:w$} | {line}\n\ |
| {le:w$} | {continued_line}\n\ |
| {s } | {underline}\n\ |
| {s } |\n\ |
| {s } = {message}", |
| s = spacing, |
| w = spacing.len(), |
| p = path, |
| ls = self.start().0, |
| le = end.0, |
| c = self.start().1, |
| line = self.line, |
| continued_line = continued_line, |
| underline = self.underline(), |
| message = self.message() |
| ) |
| } |
| } else { |
| format!( |
| "{s}--> {p}{l}:{c}\n\ |
| {s} |\n\ |
| {l} | {line}\n\ |
| {s} | {underline}\n\ |
| {s} |\n\ |
| {s} = {message}", |
| s = spacing, |
| p = path, |
| l = self.start().0, |
| c = self.start().1, |
| line = self.line, |
| underline = self.underline(), |
| message = self.message() |
| ) |
| } |
| } |
| |
| #[cfg(feature = "miette-error")] |
| /// Turns an error into a [miette](crates.io/miette) Diagnostic. |
| pub fn into_miette(self) -> impl ::miette::Diagnostic { |
| miette_adapter::MietteAdapter(self) |
| } |
| } |
| |
| impl<R: RuleType> ErrorVariant<R> { |
| /// |
| /// Returns the error message for [`ErrorVariant`] |
| /// |
| /// If [`ErrorVariant`] is [`CustomError`], it returns a |
| /// [`Cow::Borrowed`] reference to [`message`]. If [`ErrorVariant`] is [`ParsingError`], a |
| /// [`Cow::Owned`] containing "expected [ErrorVariant::ParsingError::positives] [ErrorVariant::ParsingError::negatives]" is returned. |
| /// |
| /// [`ErrorVariant`]: enum.ErrorVariant.html |
| /// [`CustomError`]: enum.ErrorVariant.html#variant.CustomError |
| /// [`ParsingError`]: enum.ErrorVariant.html#variant.ParsingError |
| /// [`Cow::Owned`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Owned |
| /// [`Cow::Borrowed`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html#variant.Borrowed |
| /// [`message`]: enum.ErrorVariant.html#variant.CustomError.field.message |
| /// # Examples |
| /// |
| /// ``` |
| /// # use pest::error::ErrorVariant; |
| /// let variant = ErrorVariant::<()>::CustomError { |
| /// message: String::from("unexpected error") |
| /// }; |
| /// |
| /// println!("{}", variant.message()); |
| pub fn message(&self) -> Cow<'_, str> { |
| match self { |
| ErrorVariant::ParsingError { |
| ref positives, |
| ref negatives, |
| } => Cow::Owned(Error::parsing_error_message(positives, negatives, |r| { |
| format!("{:?}", r) |
| })), |
| ErrorVariant::CustomError { ref message } => Cow::Borrowed(message), |
| } |
| } |
| } |
| |
| impl<R: RuleType> fmt::Display for Error<R> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}", self.format()) |
| } |
| } |
| |
| impl<R: RuleType> fmt::Display for ErrorVariant<R> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| ErrorVariant::ParsingError { .. } => write!(f, "parsing error: {}", self.message()), |
| ErrorVariant::CustomError { .. } => write!(f, "{}", self.message()), |
| } |
| } |
| } |
| |
| fn visualize_whitespace(input: &str) -> String { |
| input.to_owned().replace('\r', "␍").replace('\n', "␊") |
| } |
| |
| #[cfg(feature = "miette-error")] |
| mod miette_adapter { |
| use alloc::string::ToString; |
| use std::boxed::Box; |
| |
| use crate::error::LineColLocation; |
| |
| use super::{Error, RuleType}; |
| |
| use miette::{Diagnostic, LabeledSpan, SourceCode}; |
| |
| #[derive(thiserror::Error, Debug)] |
| #[error("Failure to parse at {:?}", self.0.line_col)] |
| pub(crate) struct MietteAdapter<R: RuleType>(pub(crate) Error<R>); |
| |
| impl<R: RuleType> Diagnostic for MietteAdapter<R> { |
| fn source_code(&self) -> Option<&dyn SourceCode> { |
| Some(&self.0.line) |
| } |
| |
| fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan>>> { |
| let message = self.0.variant.message().to_string(); |
| |
| let (offset, length) = match self.0.line_col { |
| LineColLocation::Pos((_, c)) => (c - 1, 1), |
| LineColLocation::Span((_, start_c), (_, end_c)) => { |
| (start_c - 1, end_c - start_c + 1) |
| } |
| }; |
| |
| let span = LabeledSpan::new(Some(message), offset, length); |
| |
| Some(Box::new(std::iter::once(span))) |
| } |
| |
| fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> { |
| Some(Box::new(self.0.message())) |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use alloc::vec; |
| |
| #[test] |
| fn display_parsing_error_mixed() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2, 3], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = unexpected 4, 5, or 6; expected 1, 2, or 3" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_parsing_error_positives() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2], |
| negatives: vec![], |
| }, |
| pos, |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = expected 1 or 2" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_parsing_error_negatives() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = unexpected 4, 5, or 6" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_parsing_error_unknown() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![], |
| negatives: vec![], |
| }, |
| pos, |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = unknown parsing error" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_pos() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::CustomError { |
| message: "error: big one".to_owned(), |
| }, |
| pos, |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = error: big one" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_span_two_lines() { |
| let input = "ab\ncd\nefgh"; |
| let start = Position::new(input, 4).unwrap(); |
| let end = Position::new(input, 9).unwrap(); |
| let error: Error<u32> = Error::new_from_span( |
| ErrorVariant::CustomError { |
| message: "error: big one".to_owned(), |
| }, |
| start.span(&end), |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| "3 | efgh", |
| " | ^^", |
| " |", |
| " = error: big one" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_span_three_lines() { |
| let input = "ab\ncd\nefgh"; |
| let start = Position::new(input, 1).unwrap(); |
| let end = Position::new(input, 9).unwrap(); |
| let error: Error<u32> = Error::new_from_span( |
| ErrorVariant::CustomError { |
| message: "error: big one".to_owned(), |
| }, |
| start.span(&end), |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 1:2", |
| " |", |
| "1 | ab", |
| " | ...", |
| "3 | efgh", |
| " | ^^", |
| " |", |
| " = error: big one" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_span_two_lines_inverted_cols() { |
| let input = "abcdef\ngh"; |
| let start = Position::new(input, 5).unwrap(); |
| let end = Position::new(input, 8).unwrap(); |
| let error: Error<u32> = Error::new_from_span( |
| ErrorVariant::CustomError { |
| message: "error: big one".to_owned(), |
| }, |
| start.span(&end), |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 1:6", |
| " |", |
| "1 | abcdef", |
| "2 | gh", |
| " | ^----^", |
| " |", |
| " = error: big one" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_span_end_after_newline() { |
| let input = "abcdef\n"; |
| let start = Position::new(input, 0).unwrap(); |
| let end = Position::new(input, 7).unwrap(); |
| assert!(start.at_start()); |
| assert!(end.at_end()); |
| |
| let error: Error<u32> = Error::new_from_span( |
| ErrorVariant::CustomError { |
| message: "error: big one".to_owned(), |
| }, |
| start.span(&end), |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 1:1", |
| " |", |
| "1 | abcdef␊", |
| " | ^-----^", |
| " |", |
| " = error: big one" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn display_custom_span_empty() { |
| let input = ""; |
| let start = Position::new(input, 0).unwrap(); |
| let end = Position::new(input, 0).unwrap(); |
| assert!(start.at_start()); |
| assert!(end.at_end()); |
| |
| let error: Error<u32> = Error::new_from_span( |
| ErrorVariant::CustomError { |
| message: "error: empty".to_owned(), |
| }, |
| start.span(&end), |
| ); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 1:1", |
| " |", |
| "1 | ", |
| " | ^", |
| " |", |
| " = error: empty" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn mapped_parsing_error() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2, 3], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ) |
| .renamed_rules(|n| format!("{}", n + 1)); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> 2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = unexpected 5, 6, or 7; expected 2, 3, or 4" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn error_with_path() { |
| let input = "ab\ncd\nef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2, 3], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ) |
| .with_path("file.rs"); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> file.rs:2:2", |
| " |", |
| "2 | cd", |
| " | ^---", |
| " |", |
| " = unexpected 4, 5, or 6; expected 1, 2, or 3" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn underline_with_tabs() { |
| let input = "a\txbc"; |
| let pos = Position::new(input, 2).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2, 3], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ) |
| .with_path("file.rs"); |
| |
| assert_eq!( |
| format!("{}", error), |
| [ |
| " --> file.rs:1:3", |
| " |", |
| "1 | a xbc", |
| " | ^---", |
| " |", |
| " = unexpected 4, 5, or 6; expected 1, 2, or 3" |
| ] |
| .join("\n") |
| ); |
| } |
| |
| #[test] |
| fn pos_to_lcl_conversion() { |
| let input = "input"; |
| |
| let pos = Position::new(input, 2).unwrap(); |
| |
| assert_eq!(LineColLocation::Pos(pos.line_col()), pos.into()); |
| } |
| |
| #[test] |
| fn span_to_lcl_conversion() { |
| let input = "input"; |
| |
| let span = Span::new(input, 2, 4).unwrap(); |
| let (start, end) = span.split(); |
| |
| assert_eq!( |
| LineColLocation::Span(start.line_col(), end.line_col()), |
| span.into() |
| ); |
| } |
| |
| #[cfg(feature = "miette-error")] |
| #[test] |
| fn miette_error() { |
| let input = "abc\ndef"; |
| let pos = Position::new(input, 4).unwrap(); |
| let error: Error<u32> = Error::new_from_pos( |
| ErrorVariant::ParsingError { |
| positives: vec![1, 2, 3], |
| negatives: vec![4, 5, 6], |
| }, |
| pos, |
| ); |
| |
| let miette_error = miette::Error::new(error.into_miette()); |
| |
| assert_eq!( |
| format!("{:?}", miette_error), |
| [ |
| "", |
| " \u{1b}[31m×\u{1b}[0m Failure to parse at Pos((2, 1))", |
| " ╭────", |
| " \u{1b}[2m1\u{1b}[0m │ def", |
| " · \u{1b}[35;1m┬\u{1b}[0m", |
| " · \u{1b}[35;1m╰── \u{1b}[35;1munexpected 4, 5, or 6; expected 1, 2, or 3\u{1b}[0m\u{1b}[0m", |
| " ╰────", |
| "\u{1b}[36m help: \u{1b}[0munexpected 4, 5, or 6; expected 1, 2, or 3\n" |
| ] |
| .join("\n") |
| ); |
| } |
| } |