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::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<SourceCode>,
|
||||
pub url: Option<Url>,
|
||||
pub forward: Option<Forward>,
|
||||
pub related: Option<Related>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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! {
|
||||
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 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) {
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
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::<Vec<_>>();
|
||||
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<LabeledSpan>,
|
||||
diagnostic: &(dyn Diagnostic),
|
||||
) -> fmt::Result {
|
||||
let contents = labels
|
||||
.iter()
|
||||
.map(|label| source.read_span(label.inner(), self.context_lines, self.context_lines))
|
||||
.collect::<Result<Vec<Box<dyn SpanContents<'_>>>, 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::<Vec<_>>();
|
||||
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::<Result<Vec<Box<dyn SpanContents<'_>>>, 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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ pub trait Diagnostic: std::error::Error {
|
|||
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -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<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]
|
||||
fn basic_struct() {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
|
|||
|
|
@ -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<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