blob: c569353c7b124480b6f90f4c110088da98d1a74d [file] [log] [blame]
// 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")
);
}
}