diff --git a/pest/Cargo.toml b/pest/Cargo.toml index e15d3253..a56a5343 100644 --- a/pest/Cargo.toml +++ b/pest/Cargo.toml @@ -21,6 +21,8 @@ std = ["ucd-trie/std", "dep:thiserror"] pretty-print = ["dep:serde", "dep:serde_json"] # Enable const fn constructor for `PrecClimber` const_prec_climber = [] +# Enable miette error +miette-error = ["std", "pretty-print", "dep:miette", "dep:thiserror"] [dependencies] ucd-trie = { version = "0.1.5", default-features = false } @@ -28,9 +30,11 @@ serde = { version = "1.0.145", optional = true } serde_json = { version = "1.0.85", optional = true } thiserror = { version = "1.0.37", optional = true } memchr = { version = "2", optional = true } +miette = { version = "7.2.0", optional = true, features = ["fancy"] } [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } +miette = { version = "7.2.0", features = ["fancy"] } [[bench]] name = "stack" diff --git a/pest/examples/parens.rs b/pest/examples/parens.rs index 172d1e36..f91cb53b 100644 --- a/pest/examples/parens.rs +++ b/pest/examples/parens.rs @@ -68,9 +68,20 @@ fn main() { io::stdin().read_line(&mut line).unwrap(); line.pop(); - match ParenParser::parse(Rule::expr, &line) { + let parsed = ParenParser::parse(Rule::expr, &line); + #[cfg(feature = "miette-error")] + let parsed = parsed + .map_err(Error::into_miette) + .map_err(miette::Report::from); + + match parsed { Ok(pairs) => println!("{:?}", expr(pairs)), - Err(e) => println!("\n{}", e), + // To print pest errors, use Display formatting. + #[cfg(not(feature = "miette-error"))] + Err(e) => eprintln!("\n{}", e), + // To print miette errors, use Debug formatting. + #[cfg(feature = "miette-error")] + Err(e) => eprintln!("\n{:?}", e), }; } } diff --git a/pest/src/error.rs b/pest/src/error.rs index 7ad4a1c8..0142b342 100644 --- a/pest/src/error.rs +++ b/pest/src/error.rs @@ -671,6 +671,12 @@ impl Error { ) } } + + #[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 { @@ -728,6 +734,47 @@ 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::*; @@ -1104,4 +1151,34 @@ mod tests { 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), + vec![ + " \u{1b}[31m×\u{1b}[0m Failure to parse at (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") + ); + } }