feat(related): Add related diagnostics (#68)

Fixes: https://github.com/zkat/miette/issues/47
This commit is contained in:
Kat Marchán 2021-09-21 18:34:33 -07:00 committed by GitHub
parent 491ce7c0ce
commit 25e434a2ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 260 additions and 57 deletions

View File

@ -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
}
}

View File

@ -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> + '_>>
},

View File

@ -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) {

View File

@ -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 })))
}
})
}
}

View File

@ -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(())
}

View File

@ -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> {

View File

@ -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)]

View File

@ -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(())
}