feat(reporter): fancy new reporter with unicode, colors, and multiline (#23)

Fixes: https://github.com/zkat/miette/issues/3
Fixes: https://github.com/zkat/miette/issues/5
This commit is contained in:
Kat Marchán 2021-08-20 20:09:23 -07:00 committed by GitHub
parent 8fbad1b1cd
commit d675334e48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1114 additions and 300 deletions

View File

@ -15,6 +15,8 @@ indenter = "0.3.3"
thiserror = "1.0.26"
miette-derive = { version = "=0.11.0", path = "miette-derive" }
once_cell = "1.8.0"
owo-colors = "2.0.0"
atty = "0.2.14"
[dev-dependencies]
thiserror = "1.0.26"

View File

@ -131,4 +131,5 @@ Error: Error[oops::my::bad]: oops it broke!
It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre),
and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under
the Apache License.
the Apache License. Some code is taken from
[`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.

View File

@ -6,7 +6,7 @@ use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{ParseStream, Parser};
use syn::{Ident, Index, LitStr, Member, Result, Token, braced, bracketed, parenthesized};
use syn::{braced, bracketed, parenthesized, Ident, Index, LitStr, Member, Result, Token};
#[derive(Clone)]
pub struct Display {

View File

@ -3,13 +3,13 @@
pub use miette_derive::*;
pub use error::*;
pub use printer::*;
pub use protocol::*;
pub use reporter::*;
pub use utils::*;
mod chain;
mod error;
mod printer;
mod protocol;
mod reporter;
mod source_impls;
mod utils;

View File

@ -0,0 +1,536 @@
use std::fmt;
use indenter::indented;
use owo_colors::{OwoColorize, Style};
use crate::chain::Chain;
use crate::printer::theme::*;
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
use crate::SourceSpan;
/**
Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
good enough for simple use-cases, and is the default one installed with `miette`,
but you might want to implement your own if you want custom reporting for your
tool or app.
*/
pub struct DefaultReportPrinter {
pub(crate) theme: MietteTheme,
}
impl DefaultReportPrinter {
pub fn new() -> Self {
Self {
theme: MietteTheme::default(),
}
}
pub fn new_themed(theme: MietteTheme) -> Self {
Self { theme }
}
}
impl Default for DefaultReportPrinter {
fn default() -> Self {
Self::new()
}
}
impl DefaultReportPrinter {
pub fn render_report(
&self,
f: &mut impl fmt::Write,
diagnostic: &(dyn Diagnostic),
) -> fmt::Result {
self.render_header(f, diagnostic)?;
self.render_causes(f, diagnostic)?;
if let Some(snippets) = diagnostic.snippets() {
let mut pre = false;
for snippet in snippets {
if !pre {
writeln!(f)?;
pre = true;
}
self.render_snippet(f, &snippet)?;
}
}
self.render_footer(f, diagnostic)?;
Ok(())
}
fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
let sev = match diagnostic.severity() {
Some(Severity::Error) | None => "Error".style(self.theme.styles.error),
Some(Severity::Warning) => "Warning".style(self.theme.styles.warning),
Some(Severity::Advice) => "Advice".style(self.theme.styles.advice),
}
.to_string();
let code = diagnostic.code();
let msg = diagnostic.to_string();
writeln!(
f,
"{} [{}]: {}",
sev,
code.style(self.theme.styles.code),
msg
)?;
Ok(())
}
fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
use fmt::Write as _;
if let Some(cause) = diagnostic.source() {
writeln!(f)?;
write!(f, "Caused by:")?;
let multiple = cause.source().is_some();
for (n, error) in Chain::new(cause).enumerate() {
let msg = format!("{}", error);
writeln!(f)?;
if multiple {
write!(indented(f).ind(n), "{}", msg)?;
} else {
write!(indented(f), "{}", msg)?;
}
}
}
Ok(())
}
fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
if let Some(help) = diagnostic.help() {
let help = help.style(self.theme.styles.help);
writeln!(f)?;
writeln!(f, "{} {}", self.theme.characters.eq, help)?;
}
Ok(())
}
fn render_snippet(&self, f: &mut impl fmt::Write, snippet: &DiagnosticSnippet) -> fmt::Result {
// Boring: The Header
if let Some(source_name) = snippet.context.label() {
let source_name = source_name.style(self.theme.styles.filename);
write!(f, "[{}]", source_name)?;
}
if let Some(msg) = &snippet.message {
write!(f, " {}:", msg)?;
}
writeln!(f)?;
writeln!(f)?;
// Fun time!
// Our actual code, line by line! Handy!
let lines = self.get_lines(snippet)?;
// Highlights are the bits we're going to underline in our overall
// snippet, and we need to do some analysis first to come up with
// gutter size.
let mut highlights = snippet.highlights.clone().unwrap_or_else(Vec::new);
// sorting is your friend.
highlights.sort_unstable_by_key(|h| h.offset());
let highlights = highlights
.into_iter()
.zip(self.theme.styles.highlights.iter().cloned().cycle())
.map(|(hl, st)| FancySpan::new(hl, st))
.collect::<Vec<_>>();
// The max number of gutter-lines that will be active at any given
// point. We need this to figure out indentation, so we do one loop
// over the lines to see what the damage is gonna be.
let mut max_gutter = 0usize;
for line in &lines {
let mut num_highlights = 0;
for hl in &highlights {
if !line.span_line_only(hl) && line.span_applies(hl) {
num_highlights += 1;
}
}
max_gutter = std::cmp::max(max_gutter, num_highlights);
}
// Oh and one more thing: We need to figure out how much room our line numbers need!
let linum_width = lines[..]
.last()
.expect("get_lines should always return at least one line?")
.line_number
.to_string()
.len();
// Now it's time for the fun part--actually rendering everything!
for line in &lines {
// Line number, appropriately padded.
self.write_linum(f, linum_width, line.line_number)?;
// Then, we need to print the gutter, along with any fly-bys We
// have separate gutters depending on whether we're on the actual
// line, or on one of the "highlight lines" below it.
self.render_line_gutter(f, max_gutter, line, &highlights)?;
// And _now_ we can print out the line text itself!
writeln!(f, "{}", line.text)?;
// Next, we write all the highlights that apply to this particular line.
let (single_line, multi_line): (Vec<_>, Vec<_>) = highlights
.iter()
.filter(|hl| line.span_applies(hl))
.partition(|hl| line.span_line_only(hl));
if !single_line.is_empty() {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &highlights)?;
self.render_single_line_highlights(
f,
line,
linum_width,
max_gutter,
&single_line,
&highlights,
)?;
}
for hl in multi_line {
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &highlights)?;
self.render_multi_line_end(f, hl)?;
}
}
}
Ok(())
}
fn render_line_gutter(
&self,
f: &mut impl fmt::Write,
max_gutter: usize,
line: &Line,
highlights: &[FancySpan],
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
let chars = &self.theme.characters;
let mut gutter = String::new();
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
let mut arrow = false;
for (i, hl) in applicable.enumerate() {
if line.span_starts(hl) {
gutter.push_str(&chars.ltop.to_string().style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(max_gutter.saturating_sub(i))
.style(hl.style)
.to_string(),
);
gutter.push_str(&chars.rarrow.to_string().style(hl.style).to_string());
arrow = true;
break;
} else if line.span_ends(hl) {
if hl.label().is_some() {
gutter.push_str(&chars.lcross.to_string().style(hl.style).to_string());
} else {
gutter.push_str(&chars.lbot.to_string().style(hl.style).to_string());
}
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(max_gutter.saturating_sub(i))
.style(hl.style)
.to_string(),
);
gutter.push_str(&chars.rarrow.to_string().style(hl.style).to_string());
arrow = true;
break;
} else if line.span_flyby(hl) {
gutter.push_str(&chars.vbar.to_string().style(hl.style).to_string());
} else {
gutter.push(' ');
}
}
write!(
f,
"{}{}",
gutter,
" ".repeat(
if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
)
)?;
Ok(())
}
fn render_highlight_gutter(
&self,
f: &mut impl fmt::Write,
max_gutter: usize,
line: &Line,
highlights: &[FancySpan],
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
let chars = &self.theme.characters;
let mut gutter = String::new();
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
gutter.push_str(&chars.lbot.to_string().style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(max_gutter.saturating_sub(i) + 2)
.style(hl.style)
.to_string(),
);
break;
} else {
gutter.push_str(&chars.vbar.to_string().style(hl.style).to_string());
}
}
write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
Ok(())
}
fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
write!(
f,
" {:width$} {} ",
linum,
self.theme.characters.vbar,
width = width
)?;
Ok(())
}
fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
write!(
f,
" {:width$} {} ",
"",
self.theme.characters.vbar_break,
width = width
)?;
Ok(())
}
fn render_single_line_highlights(
&self,
f: &mut impl fmt::Write,
line: &Line,
linum_width: usize,
max_gutter: usize,
single_liners: &[&FancySpan],
all_highlights: &[FancySpan],
) -> fmt::Result {
let mut underlines = String::new();
let mut highest = 0;
let chars = &self.theme.characters;
for hl in single_liners {
let local_offset = hl.offset() - line.offset;
let vbar_offset = local_offset + (hl.len() / 2);
let num_left = vbar_offset - local_offset;
let num_right = local_offset + hl.len() - vbar_offset - 1;
let start = std::cmp::max(local_offset, highest);
let end = local_offset + hl.len();
if start < end {
underlines.push_str(
&format!(
"{:width$}{}{}{}",
"",
chars.underline.to_string().repeat(num_left),
if hl.label().is_some() {
chars.underbar
} else {
chars.underline
},
chars.underline.to_string().repeat(num_right),
width = local_offset.saturating_sub(highest),
)
.style(hl.style)
.to_string(),
);
}
highest = std::cmp::max(highest, end);
}
writeln!(f, "{}", underlines)?;
for hl in single_liners {
if let Some(label) = hl.label() {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
let local_offset = hl.offset() - line.offset;
let vbar_offset = local_offset + (hl.len() / 2);
let num_right = local_offset + hl.len() - vbar_offset - 1;
let lines = format!(
"{:width$}{}{} {}",
" ",
chars.lbot,
chars.hbar.to_string().repeat(num_right + 1),
label,
width = vbar_offset
);
writeln!(f, "{}", lines.style(hl.style))?;
}
}
Ok(())
}
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
writeln!(
f,
"{} {}",
self.theme.characters.hbar.to_string().style(hl.style),
hl.label().unwrap_or_else(|| "".into()),
)?;
Ok(())
}
fn get_lines(&self, snippet: &DiagnosticSnippet) -> Result<Vec<Line>, fmt::Error> {
let context_data = snippet
.source
.read_span(&snippet.context)
.map_err(|_| fmt::Error)?;
let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
let mut line = context_data.line();
let mut column = context_data.column();
let mut offset = snippet.context.offset();
let mut line_offset = offset;
let mut iter = context.chars().peekable();
let mut line_str = String::new();
let mut lines = Vec::new();
while let Some(char) = iter.next() {
offset += char.len_utf8();
match char {
'\r' => {
if iter.next_if_eq(&'\n').is_some() {
offset += 1;
line += 1;
column = 0;
} else {
line_str.push(char);
column += 1;
}
}
'\n' => {
line += 1;
column = 0;
}
_ => {
line_str.push(char);
column += 1;
}
}
if iter.peek().is_none() {
line += 1;
}
if column == 0 || iter.peek().is_none() {
lines.push(Line {
line_number: line,
offset: line_offset,
length: offset - line_offset,
text: line_str.clone(),
});
line_str.clear();
line_offset = offset;
}
}
Ok(lines)
}
}
impl DiagnosticReportPrinter for DefaultReportPrinter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
return fmt::Debug::fmt(diagnostic, f);
}
self.render_report(f, diagnostic)
}
}
/*
Support types
*/
struct Line {
line_number: usize,
offset: usize,
length: usize,
text: String,
}
impl Line {
fn span_line_only(&self, span: &FancySpan) -> bool {
span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
}
fn span_applies(&self, span: &FancySpan) -> bool {
// Span starts in this line
(span.offset() >= self.offset && span.offset() <= self.offset +self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + span.len() > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + span.len() >= self.offset && span.offset() + span.len() <= self.offset + self.length)
}
// A "flyby" is a multi-line span that technically covers this line, but
// does not begin or end within the line itself. This method is used to
// calculate gutters.
fn span_flyby(&self, span: &FancySpan) -> bool {
// the span itself starts before this line's starting offset (so, in a prev line)
span.offset() < self.offset
// ...and it stops after this line's end.
&& span.offset() + span.len() > self.offset + self.length
}
// Does this line contain the *beginning* of this multiline span?
// This assumes self.span_applies() is true already.
fn span_starts(&self, span: &FancySpan) -> bool {
span.offset() >= self.offset
}
// Does this line contain the *end* of this multiline span?
// This assumes self.span_applies() is true already.
fn span_ends(&self, span: &FancySpan) -> bool {
span.offset() + span.len() >= self.offset
&& span.offset() + span.len() <= self.offset + self.length
}
}
struct FancySpan {
span: SourceSpan,
style: Style,
}
impl FancySpan {
fn new(span: SourceSpan, style: Style) -> Self {
FancySpan { span, style }
}
fn style(&self) -> Style {
self.style
}
fn label(&self) -> Option<String> {
self.span.label().map(|l| l.style(self.style()).to_string())
}
fn offset(&self) -> usize {
self.span.offset()
}
fn len(&self) -> usize {
self.span.len()
}
}

77
src/printer/mod.rs Normal file
View File

@ -0,0 +1,77 @@
/*!
Basic reporter for Diagnostics. Probably good enough for most use-cases,
but largely meant to be an example.
*/
use std::fmt;
use once_cell::sync::OnceCell;
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, Severity};
use crate::MietteError;
pub use default_reporter::*;
pub use theme::*;
mod default_reporter;
mod theme;
static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
OnceCell::new();
/// Set the global [DiagnosticReportPrinter] that will be used when you report
/// using [DiagnosticReport].
pub fn set_reporter(
reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
) -> Result<(), MietteError> {
REPORTER
.set(Box::new(reporter))
.map_err(|_| MietteError::ReporterInstallFailed)
}
/// Used by [DiagnosticReport] to fetch the reporter that will be used to
/// print stuff out.
pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
&**REPORTER.get_or_init(|| {
Box::new(DefaultReportPrinter {
// TODO: color support detection here?
theme: MietteTheme::default(),
})
})
}
/// Literally what it says on the tin.
pub struct JokeReporter;
impl DiagnosticReportPrinter for JokeReporter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
return fmt::Debug::fmt(diagnostic, f);
}
let sev = match diagnostic.severity() {
Some(Severity::Error) | None => "error",
Some(Severity::Warning) => "warning",
Some(Severity::Advice) => "advice",
};
writeln!(
f,
"me, with {} {}: {}",
sev,
diagnostic,
diagnostic
.help()
.unwrap_or_else(|| Box::new(&"have you tried not failing?"))
)?;
writeln!(
f,
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
diagnostic.code(),
diagnostic.snippets().map(|snippets| {
snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
.collect::<Option<Vec<String>>>()
}).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
)?;
Ok(())
}
}

165
src/printer/theme.rs Normal file
View File

@ -0,0 +1,165 @@
use atty::Stream;
use owo_colors::Style;
pub struct MietteTheme {
pub characters: MietteCharacters,
pub styles: MietteStyles,
}
impl MietteTheme {
pub fn basic() -> Self {
Self {
characters: MietteCharacters::ascii(),
styles: MietteStyles::ansi(),
}
}
pub fn unicode() -> Self {
Self {
characters: MietteCharacters::unicode(),
styles: MietteStyles::ansi(),
}
}
pub fn unicode_nocolor() -> Self {
Self {
characters: MietteCharacters::unicode(),
styles: MietteStyles::none(),
}
}
pub fn none() -> Self {
Self {
characters: MietteCharacters::ascii(),
styles: MietteStyles::none(),
}
}
}
impl Default for MietteTheme {
fn default() -> Self {
match std::env::var("NO_COLOR") {
_ if !atty::is(Stream::Stdout) || !atty::is(Stream::Stderr) => Self::basic(),
Ok(string) if string != "0" => Self::unicode_nocolor(),
_ => Self::unicode(),
}
}
}
pub struct MietteStyles {
pub error: Style,
pub warning: Style,
pub advice: Style,
pub code: Style,
pub help: Style,
pub filename: Style,
pub highlights: Vec<Style>,
}
fn style() -> Style {
Style::new()
}
impl MietteStyles {
pub fn ansi() -> Self {
Self {
error: style().red(),
warning: style().yellow(),
advice: style().cyan(),
code: style().yellow(),
help: style().cyan(),
filename: style().green(),
highlights: vec![style().red(), style().magenta(), style().cyan()],
}
}
pub fn none() -> Self {
Self {
error: style(),
warning: style(),
advice: style(),
code: style(),
help: style(),
filename: style(),
highlights: vec![style()],
}
}
}
// ---------------------------------------
// All code below here was taken from ariadne here:
// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
pub struct MietteCharacters {
pub hbar: char,
pub vbar: char,
pub xbar: char,
pub vbar_break: char,
pub uarrow: char,
pub rarrow: char,
pub ltop: char,
pub mtop: char,
pub rtop: char,
pub lbot: char,
pub rbot: char,
pub mbot: char,
pub lbox: char,
pub rbox: char,
pub lcross: char,
pub rcross: char,
pub underbar: char,
pub underline: char,
pub eq: char,
}
impl MietteCharacters {
pub fn unicode() -> Self {
Self {
hbar: '',
vbar: '',
xbar: '',
vbar_break: '·',
uarrow: '',
rarrow: '',
ltop: '',
mtop: '',
rtop: '',
lbot: '',
mbot: '',
rbot: '',
lbox: '[',
rbox: ']',
lcross: '',
rcross: '',
underbar: '',
underline: '',
eq: '',
}
}
pub fn ascii() -> Self {
Self {
hbar: '-',
vbar: '|',
xbar: '+',
vbar_break: ':',
uarrow: '^',
rarrow: '>',
ltop: ',',
mtop: 'v',
rtop: '.',
lbot: '`',
mbot: '^',
rbot: '\'',
lbox: '[',
rbox: ']',
lcross: '|',
rcross: '|',
underbar: '|',
underline: '^',
eq: '=',
}
}
}

View File

@ -4,7 +4,11 @@ that you can implement to get access to miette's (and related library's) full
reporting and such features.
*/
use std::{fmt::Display, fs, panic::Location};
use std::{
fmt::{self, Display},
fs,
panic::Location,
};
use crate::MietteError;
@ -63,6 +67,36 @@ impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'st
}
}
/**
When used with `?`/`From`, this will wrap any Diagnostics and, when
formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
use it to format the inner [Diagnostic].
*/
pub struct DiagnosticReport {
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
}
impl DiagnosticReport {
/// Return a reference to the inner [Diagnostic].
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
&*self.diagnostic
}
}
impl std::fmt::Debug for DiagnosticReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
crate::get_reporter().debug(&*self.diagnostic, f)
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
fn from(diagnostic: T) -> Self {
DiagnosticReport {
diagnostic: Box::new(diagnostic),
}
}
}
/**
Protocol for [Diagnostic] handlers, which are responsible for actually printing out Diagnostics.

View File

@ -1,243 +0,0 @@
/*!
Basic reporter for Diagnostics. Probably good enough for most use-cases,
but largely meant to be an example.
*/
use std::fmt;
use indenter::indented;
use once_cell::sync::OnceCell;
use crate::chain::Chain;
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
use crate::MietteError;
static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
OnceCell::new();
/// Set the global [DiagnosticReportPrinter] that will be used when you report
/// using [DiagnosticReport].
pub fn set_reporter(
reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
) -> Result<(), MietteError> {
REPORTER
.set(Box::new(reporter))
.map_err(|_| MietteError::ReporterInstallFailed)
}
/// Used by [DiagnosticReport] to fetch the reporter that will be used to
/// print stuff out.
pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
&**REPORTER.get_or_init(|| Box::new(DefaultReportPrinter))
}
/// Convenience alias. This is intended to be used as the return type for `main()`
pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
/// When used with `?`/`From`, this will wrap any Diagnostics and, when
/// formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
/// use it to format the inner [Diagnostic].
pub struct DiagnosticReport {
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
}
impl DiagnosticReport {
/// Return a reference to the inner [Diagnostic].
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
&*self.diagnostic
}
}
impl std::fmt::Debug for DiagnosticReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
get_reporter().debug(&*self.diagnostic, f)
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
fn from(diagnostic: T) -> Self {
DiagnosticReport {
diagnostic: Box::new(diagnostic),
}
}
}
/**
Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
good enough for simple use-cases, and is the default one installed with `miette`,
but you might want to implement your own if you want custom reporting for your
tool or app.
*/
pub struct DefaultReportPrinter;
impl DefaultReportPrinter {
fn render_snippet(
&self,
f: &mut fmt::Formatter<'_>,
snippet: &DiagnosticSnippet,
) -> fmt::Result {
use fmt::Write as _;
if let Some(source_name) = snippet.context.label() {
write!(f, "[{}]", source_name)?;
}
if let Some(msg) = &snippet.message {
write!(f, " {}:", msg)?;
}
writeln!(f)?;
writeln!(f)?;
let context_data = snippet
.source
.read_span(&snippet.context)
.map_err(|_| fmt::Error)?;
let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
let mut line = context_data.line();
let mut column = context_data.column();
let mut offset = snippet.context.offset();
let mut line_offset = offset;
let mut iter = context.chars().peekable();
let mut line_str = String::new();
let highlights = snippet.highlights.as_ref();
while let Some(char) = iter.next() {
offset += char.len_utf8();
match char {
'\r' => {
if iter.next_if_eq(&'\n').is_some() {
offset += 1;
line += 1;
column = 0;
} else {
line_str.push(char);
column += 1;
}
}
'\n' => {
line += 1;
column = 0;
}
_ => {
line_str.push(char);
column += 1;
}
}
if iter.peek().is_none() {
line += 1;
}
if column == 0 || iter.peek().is_none() {
writeln!(indented(f), "{: <2} | {}", line, line_str)?;
line_str.clear();
if let Some(highlights) = highlights {
for span in highlights {
if span.offset() >= line_offset && (span.offset() + span.len()) < offset {
// Highlight only covers one line.
write!(indented(f), "{: <2} | ", "")?;
write!(
f,
"{}{} ",
" ".repeat(span.offset() - line_offset),
"^".repeat(span.len())
)?;
if let Some(label) = span.label() {
writeln!(f, "{}", label)?;
}
} else if span.offset() < offset
&& span.offset() >= line_offset
&& (span.offset() + span.len()) >= offset
{
// Multiline highlight.
todo!("Multiline highlights.");
}
}
}
line_offset = offset;
}
}
Ok(())
}
}
impl DiagnosticReportPrinter for DefaultReportPrinter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write as _;
if f.alternate() {
return fmt::Debug::fmt(diagnostic, f);
}
let sev = match diagnostic.severity() {
Some(Severity::Error) | None => "Error",
Some(Severity::Warning) => "Warning",
Some(Severity::Advice) => "Advice",
};
writeln!(f, "{}[{}]: {}", sev, diagnostic.code(), diagnostic)?;
if let Some(cause) = diagnostic.source() {
writeln!(f)?;
write!(f, "Caused by:")?;
let multiple = cause.source().is_some();
for (n, error) in Chain::new(cause).enumerate() {
writeln!(f)?;
if multiple {
write!(indented(f).ind(n), "{}", error)?;
} else {
write!(indented(f), "{}", error)?;
}
}
}
if let Some(snippets) = diagnostic.snippets() {
let mut pre = false;
for snippet in snippets {
if !pre {
writeln!(f)?;
pre = true;
}
self.render_snippet(f, &snippet)?;
}
}
if let Some(help) = diagnostic.help() {
writeln!(f)?;
writeln!(f, "﹦{}", help)?;
}
Ok(())
}
}
/// Literally what it says on the tin.
pub struct JokeReporter;
impl DiagnosticReportPrinter for JokeReporter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
return fmt::Debug::fmt(diagnostic, f);
}
let sev = match diagnostic.severity() {
Some(Severity::Error) | None => "error",
Some(Severity::Warning) => "warning",
Some(Severity::Advice) => "advice",
};
writeln!(
f,
"me, with {} {}: {}",
sev,
diagnostic,
diagnostic
.help()
.unwrap_or_else(|| Box::new(&"have you tried not failing?"))
)?;
writeln!(
f,
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
diagnostic.code(),
diagnostic.snippets().map(|snippets| {
snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
.collect::<Option<Vec<String>>>()
}).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
)?;
Ok(())
}
}

View File

@ -2,7 +2,10 @@ use std::fmt;
use thiserror::Error;
use crate::Diagnostic;
use crate::{Diagnostic, DiagnosticReport};
/// Convenience alias. This is intended to be used as the return type for `main()`
pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
/// Convenience [Diagnostic] that can be used as an "anonymous" wrapper for
/// Errors. This is intended to be paired with [IntoDiagnostic].

View File

@ -171,7 +171,7 @@ fn fmt_help() {
Y { len: usize },
#[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))]
Z
Z,
}
assert_eq!(

289
tests/printer.rs Normal file
View File

@ -0,0 +1,289 @@
use miette::{
DefaultReportPrinter, Diagnostic, DiagnosticReport, MietteError, MietteTheme, SourceSpan,
};
use thiserror::Error;
fn fmt_report(diag: DiagnosticReport) -> String {
// Mostly for dev purposes.
let theme = if std::env::var("STYLE").is_ok() {
MietteTheme::unicode()
} else if std::env::var("BASIC").is_ok() {
MietteTheme::none()
} else {
MietteTheme::unicode_nocolor()
};
let printer = DefaultReportPrinter::new_themed(theme);
let mut out = String::new();
printer.render_report(&mut out, diag.inner()).unwrap();
out
}
#[test]
fn single_line_highlight() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight: ("this bit here", 9, 4).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn single_line_highlight_no_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight: (9, 4).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ────\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = "source\n text text text text text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("this bit here", 9, 4).into(),
highlight2: ("also this bit", 14, 4).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text text text text text\n · ──┬─ ──┬─\n · ╰── this bit here\n · ╰── also this bit\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn multiline_highlight_adjacent() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight: ("these two lines", 9, 11).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ ╭─▶ text\n 3 │ ├─▶ here\n · ╰──── these two lines\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = r#"line1
line2
line3
line4
line5
"#
.to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("block 1", 0, len).into(),
highlight2: ("block 2", 10, 9).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││ line3\n 4 │ │├─▶ line4\n · │╰──── block 2\n 6 │ ├──▶ line5\n · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn multiline_highlight_no_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = r#"line1
line2
line3
line4
line5
"#
.to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("block 1", 0, len).into(),
highlight2: (10, 9).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││ line3\n 4 │ │╰─▶ line4\n 6 │ ├──▶ line5\n · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = "source\n text\n here\nmore here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("this bit here", 0, 10).into(),
highlight2: ("also this bit", 20, 6).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭─▶ source\n 2 │ ├─▶ text\n · ╰──── this bit here\n 3 │ ╭─▶ here\n 4 │ ├─▶ more here\n · ╰──── also this bit\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
// TODO: This breaks because those highlights aren't "truly" overlapping (in absolute byte offset), but they ARE overlapping in lines. Need to detect the latter case better
#[ignore]
/// Lines are overlapping, but the offsets themselves aren't, so they _look_
/// disjunct if you only look at offsets.
fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("this bit here", 0, 8).into(),
highlight2: ("also this bit", 9, 10).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}
#[test]
/// Offsets themselves are overlapping, regardless of lines.
#[ignore]
fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
src: String,
#[snippet(src, "This is the part that broke")]
ctx: SourceSpan,
#[highlight(ctx)]
highlight1: SourceSpan,
#[highlight(ctx)]
highlight2: SourceSpan,
}
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight1: ("this bit here", 0, 8).into(),
highlight2: ("also this bit", 10, 10).into(),
};
let out = fmt_report(err.into());
println!("{}", out);
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
Ok(())
}

View File

@ -1,50 +0,0 @@
use miette::{Diagnostic, DiagnosticReport, DiagnosticSnippet, MietteError, SourceSpan};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("oops!")]
struct MyBad {
message: String,
src: String,
ctx: SourceSpan,
highlight: SourceSpan,
}
impl Diagnostic for MyBad {
fn code(&self) -> Box<dyn std::fmt::Display> {
Box::new(&"oops::my::bad")
}
fn help(&self) -> Option<Box<dyn std::fmt::Display>> {
Some(Box::new(&"try doing it better next time?"))
}
fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet> + '_>> {
Some(Box::new(
vec![DiagnosticSnippet {
message: Some(self.message.as_ref()),
source: &self.src,
context: self.ctx.clone(),
highlights: Some(vec![self.highlight.clone()]),
}]
.into_iter(),
))
}
}
#[test]
fn fancy() -> Result<(), MietteError> {
let src = "source\n text\n here".to_string();
let len = src.len();
let err = MyBad {
message: "This is the part that broke".into(),
src,
ctx: ("bad_file.rs", 0, len).into(),
highlight: ("this bit here", 9, 4).into(),
};
let rep: DiagnosticReport = err.into();
let out = format!("{:?}", rep);
// println!("{}", out);
assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 | source\n 2 | text\n ⫶ | ^^^^ this bit here\n 3 | here\n\n﹦try doing it better next time?\n".to_string(), out);
Ok(())
}