mirror of https://github.com/zkat/miette.git
feat(related): Add related diagnostics (#68)
Fixes: https://github.com/zkat/miette/issues/47
This commit is contained in:
parent
491ce7c0ce
commit
25e434a2ce
|
|
@ -7,6 +7,7 @@ use crate::diagnostic_arg::DiagnosticArg;
|
||||||
use crate::forward::{Forward, WhichFn};
|
use crate::forward::{Forward, WhichFn};
|
||||||
use crate::help::Help;
|
use crate::help::Help;
|
||||||
use crate::label::Labels;
|
use crate::label::Labels;
|
||||||
|
use crate::related::Related;
|
||||||
use crate::severity::Severity;
|
use crate::severity::Severity;
|
||||||
use crate::source_code::SourceCode;
|
use crate::source_code::SourceCode;
|
||||||
use crate::url::Url;
|
use crate::url::Url;
|
||||||
|
|
@ -64,6 +65,7 @@ pub struct DiagnosticConcreteArgs {
|
||||||
pub source_code: Option<SourceCode>,
|
pub source_code: Option<SourceCode>,
|
||||||
pub url: Option<Url>,
|
pub url: Option<Url>,
|
||||||
pub forward: Option<Forward>,
|
pub forward: Option<Forward>,
|
||||||
|
pub related: Option<Related>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticConcreteArgs {
|
impl DiagnosticConcreteArgs {
|
||||||
|
|
@ -103,9 +105,11 @@ impl DiagnosticConcreteArgs {
|
||||||
}
|
}
|
||||||
let labels = Labels::from_fields(fields)?;
|
let labels = Labels::from_fields(fields)?;
|
||||||
let source_code = SourceCode::from_fields(fields)?;
|
let source_code = SourceCode::from_fields(fields)?;
|
||||||
|
let related = Related::from_fields(fields)?;
|
||||||
let concrete = DiagnosticConcreteArgs {
|
let concrete = DiagnosticConcreteArgs {
|
||||||
code,
|
code,
|
||||||
help,
|
help,
|
||||||
|
related,
|
||||||
severity,
|
severity,
|
||||||
labels,
|
labels,
|
||||||
url,
|
url,
|
||||||
|
|
@ -215,6 +219,7 @@ impl Diagnostic {
|
||||||
let labels_method = forward.gen_struct_method(WhichFn::Labels);
|
let labels_method = forward.gen_struct_method(WhichFn::Labels);
|
||||||
let source_code_method = forward.gen_struct_method(WhichFn::SourceCode);
|
let source_code_method = forward.gen_struct_method(WhichFn::SourceCode);
|
||||||
let severity_method = forward.gen_struct_method(WhichFn::Severity);
|
let severity_method = forward.gen_struct_method(WhichFn::Severity);
|
||||||
|
let related_method = forward.gen_struct_method(WhichFn::Related);
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||||
|
|
@ -224,6 +229,7 @@ impl Diagnostic {
|
||||||
#labels_method
|
#labels_method
|
||||||
#severity_method
|
#severity_method
|
||||||
#source_code_method
|
#source_code_method
|
||||||
|
#related_method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +255,11 @@ impl Diagnostic {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|x| x.gen_struct())
|
.and_then(|x| x.gen_struct())
|
||||||
.or_else(|| forward(WhichFn::Severity));
|
.or_else(|| forward(WhichFn::Severity));
|
||||||
|
let rel_body = concrete
|
||||||
|
.related
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.gen_struct())
|
||||||
|
.or_else(|| forward(WhichFn::Related));
|
||||||
let url_body = concrete
|
let url_body = concrete
|
||||||
.url
|
.url
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -269,6 +280,7 @@ impl Diagnostic {
|
||||||
#code_body
|
#code_body
|
||||||
#help_body
|
#help_body
|
||||||
#sev_body
|
#sev_body
|
||||||
|
#rel_body
|
||||||
#url_body
|
#url_body
|
||||||
#labels_body
|
#labels_body
|
||||||
#src_body
|
#src_body
|
||||||
|
|
@ -288,6 +300,7 @@ impl Diagnostic {
|
||||||
let sev_body = Severity::gen_enum(variants);
|
let sev_body = Severity::gen_enum(variants);
|
||||||
let labels_body = Labels::gen_enum(variants);
|
let labels_body = Labels::gen_enum(variants);
|
||||||
let src_body = SourceCode::gen_enum(variants);
|
let src_body = SourceCode::gen_enum(variants);
|
||||||
|
let rel_body = Related::gen_enum(variants);
|
||||||
let url_body = Url::gen_enum(ident, variants);
|
let url_body = Url::gen_enum(ident, variants);
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||||
|
|
@ -296,6 +309,7 @@ impl Diagnostic {
|
||||||
#sev_body
|
#sev_body
|
||||||
#labels_body
|
#labels_body
|
||||||
#src_body
|
#src_body
|
||||||
|
#rel_body
|
||||||
#url_body
|
#url_body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ pub enum WhichFn {
|
||||||
Severity,
|
Severity,
|
||||||
Labels,
|
Labels,
|
||||||
SourceCode,
|
SourceCode,
|
||||||
|
Related,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WhichFn {
|
impl WhichFn {
|
||||||
|
|
@ -48,6 +49,7 @@ impl WhichFn {
|
||||||
Self::Severity => quote! { severity() },
|
Self::Severity => quote! { severity() },
|
||||||
Self::Labels => quote! { labels() },
|
Self::Labels => quote! { labels() },
|
||||||
Self::SourceCode => quote! { source_code() },
|
Self::SourceCode => quote! { source_code() },
|
||||||
|
Self::Related => quote! { related() },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +67,9 @@ impl WhichFn {
|
||||||
Self::Severity => quote! {
|
Self::Severity => quote! {
|
||||||
fn severity(&self) -> std::option::Option<miette::Severity>
|
fn severity(&self) -> std::option::Option<miette::Severity>
|
||||||
},
|
},
|
||||||
|
Self::Related => quote! {
|
||||||
|
fn related(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &dyn miette::Diagnostic> + '_>>
|
||||||
|
},
|
||||||
Self::Labels => quote! {
|
Self::Labels => quote! {
|
||||||
fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>>
|
fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>>
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ mod fmt;
|
||||||
mod forward;
|
mod forward;
|
||||||
mod help;
|
mod help;
|
||||||
mod label;
|
mod label;
|
||||||
|
mod related;
|
||||||
mod severity;
|
mod severity;
|
||||||
mod source_code;
|
mod source_code;
|
||||||
mod url;
|
mod url;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[proc_macro_derive(Diagnostic, attributes(diagnostic, label, source_code))]
|
#[proc_macro_derive(Diagnostic, attributes(diagnostic, source_code, label, related))]
|
||||||
pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
let cmd = match Diagnostic::from_derive_input(input) {
|
let cmd = match Diagnostic::from_derive_input(input) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
|
||||||
|
forward::WhichFn,
|
||||||
|
utils::{display_pat_members, gen_all_variants_with},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Related(syn::Member);
|
||||||
|
|
||||||
|
impl Related {
|
||||||
|
pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
|
||||||
|
match fields {
|
||||||
|
syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
|
||||||
|
syn::Fields::Unnamed(unnamed) => {
|
||||||
|
Self::from_fields_vec(unnamed.unnamed.iter().collect())
|
||||||
|
}
|
||||||
|
syn::Fields::Unit => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
|
||||||
|
for (i, field) in fields.iter().enumerate() {
|
||||||
|
for attr in &field.attrs {
|
||||||
|
if attr.path.is_ident("related") {
|
||||||
|
let related = if let Some(ident) = field.ident.clone() {
|
||||||
|
syn::Member::Named(ident)
|
||||||
|
} else {
|
||||||
|
syn::Member::Unnamed(syn::Index {
|
||||||
|
index: i as u32,
|
||||||
|
span: field.span(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return Ok(Some(Related(related)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
|
||||||
|
gen_all_variants_with(
|
||||||
|
variants,
|
||||||
|
WhichFn::Related,
|
||||||
|
|ident, fields, DiagnosticConcreteArgs { related, .. }| {
|
||||||
|
let (display_pat, _display_members) = display_pat_members(fields);
|
||||||
|
related.as_ref().map(|related| {
|
||||||
|
let rel = match &related.0 {
|
||||||
|
syn::Member::Named(ident) => ident.clone(),
|
||||||
|
syn::Member::Unnamed(syn::Index { index, .. }) => {
|
||||||
|
format_ident!("_{}", index)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
Self::#ident #display_pat => {
|
||||||
|
std::option::Option::Some(std::boxed::Box::new(
|
||||||
|
#rel.iter().map(|x| -> &(dyn Diagnostic) { &*x })
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
|
||||||
|
let rel = &self.0;
|
||||||
|
Some(quote! {
|
||||||
|
fn related<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
|
||||||
|
std::option::Option::Some(std::boxed::Box::new(self.#rel.iter().map(|x| -> &(dyn Diagnostic) { &*x })))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,19 +102,17 @@ impl GraphicalReportHandler {
|
||||||
self.render_header(f, diagnostic)?;
|
self.render_header(f, diagnostic)?;
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
self.render_causes(f, diagnostic)?;
|
self.render_causes(f, diagnostic)?;
|
||||||
|
self.render_snippets(f, diagnostic)?;
|
||||||
if let Some(source) = diagnostic.source_code() {
|
|
||||||
if let Some(labels) = diagnostic.labels() {
|
|
||||||
let mut labels = labels.collect::<Vec<_>>();
|
|
||||||
labels.sort_unstable_by_key(|l| l.inner().offset());
|
|
||||||
if !labels.is_empty() {
|
|
||||||
writeln!(f)?;
|
|
||||||
self.render_snippets(f, source, labels)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.render_footer(f, diagnostic)?;
|
self.render_footer(f, diagnostic)?;
|
||||||
|
self.render_related(f, diagnostic)?;
|
||||||
|
if let Some(footer) = &self.footer {
|
||||||
|
writeln!(f)?;
|
||||||
|
let width = self.termwidth.saturating_sub(4);
|
||||||
|
let opts = textwrap::Options::new(width)
|
||||||
|
.initial_indent(" ")
|
||||||
|
.subsequent_indent(" ");
|
||||||
|
writeln!(f, "{}", textwrap::fill(footer, opts))?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,13 +212,25 @@ impl GraphicalReportHandler {
|
||||||
.subsequent_indent(" ");
|
.subsequent_indent(" ");
|
||||||
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
|
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
|
||||||
}
|
}
|
||||||
if let Some(footer) = &self.footer {
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_related(
|
||||||
|
&self,
|
||||||
|
f: &mut impl fmt::Write,
|
||||||
|
diagnostic: &(dyn Diagnostic),
|
||||||
|
) -> fmt::Result {
|
||||||
|
if let Some(related) = diagnostic.related() {
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
let width = self.termwidth.saturating_sub(4);
|
for rel in related {
|
||||||
let opts = textwrap::Options::new(width)
|
write!(f, "Error: ")?;
|
||||||
.initial_indent(" ")
|
self.render_header(f, rel)?;
|
||||||
.subsequent_indent(" ");
|
writeln!(f)?;
|
||||||
writeln!(f, "{}", textwrap::fill(footer, opts))?;
|
self.render_causes(f, rel)?;
|
||||||
|
self.render_snippets(f, rel)?;
|
||||||
|
self.render_footer(f, rel)?;
|
||||||
|
self.render_related(f, rel)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -228,49 +238,63 @@ impl GraphicalReportHandler {
|
||||||
fn render_snippets(
|
fn render_snippets(
|
||||||
&self,
|
&self,
|
||||||
f: &mut impl fmt::Write,
|
f: &mut impl fmt::Write,
|
||||||
source: &dyn SourceCode,
|
diagnostic: &(dyn Diagnostic),
|
||||||
labels: Vec<LabeledSpan>,
|
|
||||||
) -> fmt::Result {
|
) -> fmt::Result {
|
||||||
let contents = labels
|
if let Some(source) = diagnostic.source_code() {
|
||||||
.iter()
|
if let Some(labels) = diagnostic.labels() {
|
||||||
.map(|label| source.read_span(label.inner(), self.context_lines, self.context_lines))
|
let mut labels = labels.collect::<Vec<_>>();
|
||||||
.collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
|
labels.sort_unstable_by_key(|l| l.inner().offset());
|
||||||
.map_err(|_| fmt::Error)?;
|
if !labels.is_empty() {
|
||||||
let contexts = labels.iter().cloned().zip(contents.iter()).coalesce(
|
writeln!(f)?;
|
||||||
|(left, left_conts), (right, right_conts)| {
|
let contents = labels
|
||||||
let left_end = left.offset() + left.len();
|
.iter()
|
||||||
let right_end = right.offset() + right.len();
|
.map(|label| {
|
||||||
if left_conts.line() + left_conts.line_count() >= right_conts.line() {
|
source.read_span(label.inner(), self.context_lines, self.context_lines)
|
||||||
// The snippets will overlap, so we create one Big Chunky Boi
|
})
|
||||||
let new_span = LabeledSpan::new(
|
.collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
|
||||||
left.label().map(String::from),
|
.map_err(|_| fmt::Error)?;
|
||||||
left.offset(),
|
let contexts = labels.iter().cloned().zip(contents.iter()).coalesce(
|
||||||
if right_end >= left_end {
|
|(left, left_conts), (right, right_conts)| {
|
||||||
// Right end goes past left end
|
let left_end = left.offset() + left.len();
|
||||||
right_end - left.offset()
|
let right_end = right.offset() + right.len();
|
||||||
} else {
|
if left_conts.line() + left_conts.line_count() >= right_conts.line() {
|
||||||
// right is contained inside left
|
// The snippets will overlap, so we create one Big Chunky Boi
|
||||||
left.len()
|
let new_span = LabeledSpan::new(
|
||||||
|
left.label().map(String::from),
|
||||||
|
left.offset(),
|
||||||
|
if right_end >= left_end {
|
||||||
|
// Right end goes past left end
|
||||||
|
right_end - left.offset()
|
||||||
|
} else {
|
||||||
|
// right is contained inside left
|
||||||
|
left.len()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if source
|
||||||
|
.read_span(
|
||||||
|
new_span.inner(),
|
||||||
|
self.context_lines,
|
||||||
|
self.context_lines,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
Ok((
|
||||||
|
new_span, // We'll throw this away later
|
||||||
|
left_conts,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(((left, left_conts), (right, right_conts)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(((left, left_conts), (right, right_conts)))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if source
|
for (ctx, _) in contexts {
|
||||||
.read_span(new_span.inner(), self.context_lines, self.context_lines)
|
self.render_context(f, source, &ctx, &labels[..])?;
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
Ok((
|
|
||||||
new_span, // We'll throw this away later
|
|
||||||
left_conts,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err(((left, left_conts), (right, right_conts)))
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err(((left, left_conts), (right, right_conts)))
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
for (ctx, _) in contexts {
|
|
||||||
self.render_context(f, source, &ctx, &labels[..])?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ pub trait Diagnostic: std::error::Error {
|
||||||
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Additional related Diagnostics.
|
||||||
|
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for Box<dyn Diagnostic> {
|
impl std::error::Error for Box<dyn Diagnostic> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,37 @@
|
||||||
use miette::{Diagnostic, Severity, SourceSpan};
|
use miette::{Diagnostic, Severity, SourceSpan};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn related() {
|
||||||
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
|
#[error("welp")]
|
||||||
|
#[diagnostic(code(foo::bar::baz))]
|
||||||
|
struct Foo {
|
||||||
|
#[related]
|
||||||
|
related: Vec<Baz>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
|
enum Bar {
|
||||||
|
#[error("variant1")]
|
||||||
|
#[diagnostic(code(foo::bar::baz))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Bad {
|
||||||
|
#[related]
|
||||||
|
related: Vec<Baz>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("variant2")]
|
||||||
|
#[diagnostic(code(foo::bar::baz))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
LessBad(#[related] Vec<Baz>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
|
#[error("welp2")]
|
||||||
|
struct Baz;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic_struct() {
|
fn basic_struct() {
|
||||||
#[derive(Debug, Diagnostic, Error)]
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
|
|
||||||
|
|
@ -675,3 +675,50 @@ fn disable_url_links() -> Result<(), MietteError> {
|
||||||
assert!(out.contains("oops::my::bad"));
|
assert!(out.contains("oops::my::bad"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn related() -> Result<(), MietteError> {
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
#[error("oops!")]
|
||||||
|
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||||
|
struct MyBad {
|
||||||
|
#[source_code]
|
||||||
|
src: NamedSource,
|
||||||
|
#[label("this bit here")]
|
||||||
|
highlight: SourceSpan,
|
||||||
|
#[related]
|
||||||
|
related: Vec<MyBad>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = "source\n text\n here".to_string();
|
||||||
|
let err = MyBad {
|
||||||
|
src: NamedSource::new("bad_file.rs", src.clone()),
|
||||||
|
highlight: (9, 4).into(),
|
||||||
|
related: vec![MyBad {
|
||||||
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
|
highlight: (0, 6).into(),
|
||||||
|
related: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let out = fmt_report(err.into());
|
||||||
|
println!("Error: {}", out);
|
||||||
|
let expected = r#"
|
||||||
|
────[oops::my::bad]──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
× oops!
|
||||||
|
|
||||||
|
╭───[bad_file.rs:1:1] This is the part that broke:
|
||||||
|
1 │ source
|
||||||
|
2 │ text
|
||||||
|
· ──┬─
|
||||||
|
· ╰── this bit here
|
||||||
|
3 │ here
|
||||||
|
╰───
|
||||||
|
|
||||||
|
‽ try doing it better next time?
|
||||||
|
"#
|
||||||
|
.trim_start()
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(expected, out);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue