use std::collections::HashSet;
use lazy_static::lazy_static;
use pest::error::ErrorVariant;
use pest::error::InputLocation;
use pest::RuleType;
use serde::Deserialize;
use serde::Serialize;
use crate::file::location::Position;
use crate::file::Location;
lazy_static! {
static ref IGNORED_POSITIVE_TOKENS_SET: HashSet<&'static str> = {
let mut set = HashSet::new();
set.insert("comment");
set.insert("whitespace");
set
};
static ref IGNORED_NEGATIVE_TOKENS_SET: HashSet<&'static str> = {
HashSet::new()
};
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Error {
message: String,
location: Location,
}
impl Error {
pub fn new(message: impl Into<String>, location: Location) -> Self {
let message = message.into();
Self { message, location }
}
pub fn message(&self) -> &str {
self.message.as_ref()
}
pub fn into_message(self) -> String {
self.message
}
pub fn byte_range(&self) -> Option<std::ops::Range<usize>> {
match &self.location {
Location::Unplaced => None,
Location::Position(position) => Some(position.byte_no()..position.byte_no()),
Location::Span { start, end } => Some(start.byte_no()..end.byte_no()),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)?;
if self.location != Location::Unplaced {
write!(f, " ({})", self.location)?;
}
Ok(())
}
}
impl<R: RuleType> From<pest::error::Error<R>> for Error {
fn from(err: pest::error::Error<R>) -> Self {
let (start_byte_no, end_byte_no) = match err.location {
InputLocation::Pos(pos) => (pos, pos),
InputLocation::Span((start, end)) => (start, end),
};
let location = match err.line_col {
pest::error::LineColLocation::Pos((line_no, col_no)) => Location::Position(
Position::try_new(line_no, col_no, start_byte_no)
.expect("Pest should return line and column numbers that are one or greater"),
),
pest::error::LineColLocation::Span(start, end) => Location::Span {
start: Position::try_new(start.0, start.1, start_byte_no)
.expect("Pest should return line and column numbers that are one or greater"),
end: Position::try_new(end.0, end.1, end_byte_no)
.expect("Pest should return line and column numbers that are one or greater"),
},
};
let message = match err.variant {
ErrorVariant::ParsingError {
positives,
negatives,
} => {
let mut parts = Vec::new();
let positives = filter(positives, &IGNORED_POSITIVE_TOKENS_SET).join(", ");
if !positives.is_empty() {
parts.push(format!("The following tokens are required: {}.", positives));
}
let negatives = filter(negatives, &IGNORED_NEGATIVE_TOKENS_SET).join(", ");
if !negatives.is_empty() {
parts.push(format!(
"The following tokens are not allowed: {}.",
negatives
));
}
if parts.is_empty() {
panic!("Pest should return either a positive or negative ruleset")
}
parts.join(" ")
}
ErrorVariant::CustomError { message } => message,
};
Error { message, location }
}
}
fn filter<R: RuleType>(rules: Vec<R>, ignored: &HashSet<&str>) -> Vec<String> {
let rules = rules
.into_iter()
.map(|rule| format!("{:?}", rule).to_lowercase())
.collect::<Vec<_>>();
if rules.iter().all(|p| ignored.contains(p.as_str())) {
return rules;
}
rules
.into_iter()
.filter(|p| !ignored.contains(p.as_str()))
.collect()
}