diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 50d47c4e..11e36b29 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -7,6 +7,7 @@ use crate::diagnostic_arg::DiagnosticArg; use crate::forward::{Forward, WhichFn}; use crate::help::Help; use crate::label::Labels; +use crate::related::Related; use crate::severity::Severity; use crate::source_code::SourceCode; use crate::url::Url; @@ -64,6 +65,7 @@ pub struct DiagnosticConcreteArgs { pub source_code: Option, pub url: Option, pub forward: Option, + pub related: Option, } impl DiagnosticConcreteArgs { @@ -103,9 +105,11 @@ impl DiagnosticConcreteArgs { } let labels = Labels::from_fields(fields)?; let source_code = SourceCode::from_fields(fields)?; + let related = Related::from_fields(fields)?; let concrete = DiagnosticConcreteArgs { code, help, + related, severity, labels, url, @@ -215,6 +219,7 @@ impl Diagnostic { let labels_method = forward.gen_struct_method(WhichFn::Labels); let source_code_method = forward.gen_struct_method(WhichFn::SourceCode); let severity_method = forward.gen_struct_method(WhichFn::Severity); + let related_method = forward.gen_struct_method(WhichFn::Related); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { @@ -224,6 +229,7 @@ impl Diagnostic { #labels_method #severity_method #source_code_method + #related_method } } } @@ -249,6 +255,11 @@ impl Diagnostic { .as_ref() .and_then(|x| x.gen_struct()) .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 .url .as_ref() @@ -269,6 +280,7 @@ impl Diagnostic { #code_body #help_body #sev_body + #rel_body #url_body #labels_body #src_body @@ -288,6 +300,7 @@ impl Diagnostic { let sev_body = Severity::gen_enum(variants); let labels_body = Labels::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); quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { @@ -296,6 +309,7 @@ impl Diagnostic { #sev_body #labels_body #src_body + #rel_body #url_body } } diff --git a/miette-derive/src/forward.rs b/miette-derive/src/forward.rs index 3d765ab4..ca7e1b38 100644 --- a/miette-derive/src/forward.rs +++ b/miette-derive/src/forward.rs @@ -37,6 +37,7 @@ pub enum WhichFn { Severity, Labels, SourceCode, + Related, } impl WhichFn { @@ -48,6 +49,7 @@ impl WhichFn { Self::Severity => quote! { severity() }, Self::Labels => quote! { labels() }, Self::SourceCode => quote! { source_code() }, + Self::Related => quote! { related() }, } } @@ -65,6 +67,9 @@ impl WhichFn { Self::Severity => quote! { fn severity(&self) -> std::option::Option }, + Self::Related => quote! { + fn related(&self) -> std::option::Option + '_>> + }, Self::Labels => quote! { fn labels(&self) -> std::option::Option + '_>> }, diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index c65b3f52..1672c4b4 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -10,12 +10,13 @@ mod fmt; mod forward; mod help; mod label; +mod related; mod severity; mod source_code; mod url; 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 { let input = parse_macro_input!(input as DeriveInput); let cmd = match Diagnostic::from_derive_input(input) { diff --git a/miette-derive/src/related.rs b/miette-derive/src/related.rs new file mode 100644 index 00000000..b2342a3a --- /dev/null +++ b/miette-derive/src/related.rs @@ -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> { + 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> { + 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 { + 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 { + let rel = &self.0; + Some(quote! { + fn related<'a>(&'a self) -> std::option::Option + 'a>> { + std::option::Option::Some(std::boxed::Box::new(self.#rel.iter().map(|x| -> &(dyn Diagnostic) { &*x }))) + } + }) + } +} diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 3edc35f3..101db42e 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -102,19 +102,17 @@ impl GraphicalReportHandler { self.render_header(f, diagnostic)?; writeln!(f)?; self.render_causes(f, diagnostic)?; - - if let Some(source) = diagnostic.source_code() { - if let Some(labels) = diagnostic.labels() { - let mut labels = labels.collect::>(); - labels.sort_unstable_by_key(|l| l.inner().offset()); - if !labels.is_empty() { - writeln!(f)?; - self.render_snippets(f, source, labels)?; - } - } - } - + self.render_snippets(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(()) } @@ -214,13 +212,25 @@ impl GraphicalReportHandler { .subsequent_indent(" "); 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)?; - let width = self.termwidth.saturating_sub(4); - let opts = textwrap::Options::new(width) - .initial_indent(" ") - .subsequent_indent(" "); - writeln!(f, "{}", textwrap::fill(footer, opts))?; + for rel in related { + write!(f, "Error: ")?; + self.render_header(f, rel)?; + writeln!(f)?; + self.render_causes(f, rel)?; + self.render_snippets(f, rel)?; + self.render_footer(f, rel)?; + self.render_related(f, rel)?; + } } Ok(()) } @@ -228,49 +238,63 @@ impl GraphicalReportHandler { fn render_snippets( &self, f: &mut impl fmt::Write, - source: &dyn SourceCode, - labels: Vec, + diagnostic: &(dyn Diagnostic), ) -> fmt::Result { - let contents = labels - .iter() - .map(|label| source.read_span(label.inner(), self.context_lines, self.context_lines)) - .collect::>>, MietteError>>() - .map_err(|_| fmt::Error)?; - let contexts = labels.iter().cloned().zip(contents.iter()).coalesce( - |(left, left_conts), (right, right_conts)| { - let left_end = left.offset() + left.len(); - let right_end = right.offset() + right.len(); - if left_conts.line() + left_conts.line_count() >= right_conts.line() { - // The snippets will overlap, so we create one Big Chunky Boi - 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 let Some(source) = diagnostic.source_code() { + if let Some(labels) = diagnostic.labels() { + let mut labels = labels.collect::>(); + labels.sort_unstable_by_key(|l| l.inner().offset()); + if !labels.is_empty() { + writeln!(f)?; + let contents = labels + .iter() + .map(|label| { + source.read_span(label.inner(), self.context_lines, self.context_lines) + }) + .collect::>>, MietteError>>() + .map_err(|_| fmt::Error)?; + let contexts = labels.iter().cloned().zip(contents.iter()).coalesce( + |(left, left_conts), (right, right_conts)| { + let left_end = left.offset() + left.len(); + let right_end = right.offset() + right.len(); + if left_conts.line() + left_conts.line_count() >= right_conts.line() { + // The snippets will overlap, so we create one Big Chunky Boi + 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 - .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))) + for (ctx, _) in contexts { + self.render_context(f, source, &ctx, &labels[..])?; } - } else { - Err(((left, left_conts), (right, right_conts))) } - }, - ); - for (ctx, _) in contexts { - self.render_context(f, source, &ctx, &labels[..])?; + } } Ok(()) } diff --git a/src/protocol.rs b/src/protocol.rs index 872d525b..0358bec0 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -54,6 +54,11 @@ pub trait Diagnostic: std::error::Error { fn labels(&self) -> Option + '_>> { None } + + /// Additional related Diagnostics. + fn related<'a>(&'a self) -> Option + 'a>> { + None + } } impl std::error::Error for Box { diff --git a/tests/derive.rs b/tests/derive.rs index 2a7f3546..f1767c48 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -1,6 +1,37 @@ use miette::{Diagnostic, Severity, SourceSpan}; use thiserror::Error; +#[test] +fn related() { + #[derive(Error, Debug, Diagnostic)] + #[error("welp")] + #[diagnostic(code(foo::bar::baz))] + struct Foo { + #[related] + related: Vec, + } + + #[derive(Error, Debug, Diagnostic)] + enum Bar { + #[error("variant1")] + #[diagnostic(code(foo::bar::baz))] + #[allow(dead_code)] + Bad { + #[related] + related: Vec, + }, + + #[error("variant2")] + #[diagnostic(code(foo::bar::baz))] + #[allow(dead_code)] + LessBad(#[related] Vec), + } + + #[derive(Error, Debug, Diagnostic)] + #[error("welp2")] + struct Baz; +} + #[test] fn basic_struct() { #[derive(Debug, Diagnostic, Error)] diff --git a/tests/graphical.rs b/tests/graphical.rs index 64f26985..c3555a24 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -675,3 +675,50 @@ fn disable_url_links() -> Result<(), MietteError> { assert!(out.contains("oops::my::bad")); 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, + } + + 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(()) +}