use miette::{Diagnostic as MietteDiagnostic, GraphicalReportHandler, GraphicalTheme};
use miette::{LabeledSpan, NamedSource, SourceCode, SourceSpan};
use std::fmt;
use crate::parser::ParseError;
use crate::token::Span;
#[derive(Debug, Clone)]
pub struct Diagnostic {
message: String,
span: Option<SourceSpan>,
help: Option<String>,
}
impl Diagnostic {
pub fn new(message: impl Into<String>, span: Option<Span>, help: Option<String>) -> Self {
Self {
message: message.into(),
span: span.map(to_source_span),
help,
}
}
}
fn to_source_span(span: Span) -> SourceSpan {
SourceSpan::new(span.start.into(), span.end.saturating_sub(span.start))
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for Diagnostic {}
impl MietteDiagnostic for Diagnostic {
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.help
.as_ref()
.map(|help| Box::new(help) as Box<dyn fmt::Display + 'a>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let span = self.span?;
Some(Box::new(std::iter::once(LabeledSpan::underline(span))))
}
}
impl ParseError {
pub fn into_diagnostic(self) -> Diagnostic {
Diagnostic::new(self.message, Some(self.span), self.help)
}
}
pub fn render_diagnostic(source: &str, path: &str, err: &Diagnostic) -> String {
let report = WithSource {
source: NamedSource::new(path, source.to_string()),
inner: err,
};
let mut rendered = String::new();
handler()
.render_report(&mut rendered, &report)
.expect("rendering a diagnostic into a String cannot fail");
rendered
}
fn handler() -> GraphicalReportHandler {
GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()).with_width(80)
}
struct WithSource<'a> {
source: NamedSource<String>,
inner: &'a Diagnostic,
}
impl fmt::Debug for WithSource<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.inner, f)
}
}
impl fmt::Display for WithSource<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.inner, f)
}
}
impl std::error::Error for WithSource<'_> {}
impl MietteDiagnostic for WithSource<'_> {
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.inner.help()
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.inner.labels()
}
fn source_code(&self) -> Option<&dyn SourceCode> {
Some(&self.source)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn renders_zero_width_eof_span_without_panicking() {
let src = "beam x {";
let err = parse(src).unwrap_err();
assert_eq!(err.span.start, err.span.end, "expected a zero-width span");
let rendered = render_diagnostic(src, "Beamfile", &err.into_diagnostic());
assert!(!rendered.is_empty());
assert!(rendered.contains("Beamfile"));
}
#[test]
fn renders_a_diagnostic_without_a_span_as_a_plain_sentence() {
let diagnostic = Diagnostic::new(
"unknown beam `biuld`",
None,
Some("did you mean `build`?".to_string()),
);
let rendered = render_diagnostic("beam build { run \"x\" }", "Beamfile", &diagnostic);
assert!(rendered.contains("unknown beam `biuld`"));
assert!(rendered.contains("did you mean `build`?"));
assert!(
!rendered.contains("Beamfile:"),
"no source position may be asserted, got: {rendered}"
);
}
}