// pest. The Elegant Parser // Copyright (c) 2018 Dragoș Tiselice // // Licensed under the Apache License, Version 2.0 // or the MIT // license , 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 { /// Variant of the error pub variant: ErrorVariant, /// Location within the input string pub location: InputLocation, /// Line/column within the input string pub line_col: LineColLocation, path: Option, line: String, continued_line: Option, parse_attempts: Option>, } /// Different kinds of parsing errors. #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum ErrorVariant { /// Generated parsing error with expected and unexpected `Rule`s ParsingError { /// Positive attempts positives: Vec, /// Negative attempts negatives: Vec, }, /// 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> for LineColLocation { fn from(value: Position<'_>) -> Self { Self::Pos(value.line_col()) } } impl From> 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 = Box Option>; /// Function mapping string element to bool denoting whether it's a whitespace defined by user. pub type IsWhitespaceFn = Box 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 ParseAttempts { /// 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 { 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 = 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::>() .join(", "), ); helper_messages.push(helper_tokens_message); } helper_messages } } impl Error { /// 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, pos: Position<'_>) -> Error { 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, pos: Position<'_>, parse_attempts: ParseAttempts, ) -> Error { 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, span: Span<'_>) -> Error { 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 { 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(mut self, f: F) -> Error 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> { 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, is_whitespace: &IsWhitespaceFn, ) -> Option> { 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 = 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, Vec>> = 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(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(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::>() .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 ErrorVariant { /// /// 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 fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.format()) } } impl fmt::Display for ErrorVariant { 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(pub(crate) Error); impl Diagnostic for MietteAdapter { fn source_code(&self) -> Option<&dyn SourceCode> { Some(&self.0.line) } fn labels(&self) -> Option>> { 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> { 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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") ); } }