feat(help): update macro to allow optional help text

Fixes: https://github.com/zkat/miette/issues/148
This commit is contained in:
Kat Marchán 2022-04-17 18:04:59 -07:00
parent 5e54b29acf
commit cb10790d8e
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
7 changed files with 205 additions and 25 deletions

View File

@ -9,3 +9,9 @@ workspace=false
install_crate="cargo-release" install_crate="cargo-release"
command = "cargo" command = "cargo"
args = ["release", "--workspace", "${@}"] args = ["release", "--workspace", "${@}"]
[tasks.readme]
workspace=false
install_crate="cargo-readme"
command = "cargo"
args = ["readme"]

View File

@ -372,6 +372,34 @@ pub struct MyErrorType {
} }
``` ```
#### ... help text
`miette` provides two facilities for supplying help text for your errors:
The first is the `#[help()]` format attribute that applies to structs or enum variants:
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(help("try doing this instead"))]
struct Foo;
```
The other is by programmatically supplying the help text as a field to your
diagnostic:
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Foo {
#[help]
advice: Option<String>
}
let err = Foo { advice: Some("try doing this instead".to_string()) };
```
#### ... multiple related errors #### ... multiple related errors
`miette` supports collecting multiple errors into a single diagnostic, and `miette` supports collecting multiple errors into a single diagnostic, and

View File

@ -73,9 +73,10 @@ 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 related = Related::from_fields(fields)?;
let help = Help::from_fields(fields)?;
Ok(DiagnosticConcreteArgs { Ok(DiagnosticConcreteArgs {
code: None, code: None,
help: None, help,
related, related,
severity: None, severity: None,
labels, labels,

View File

@ -1,8 +1,9 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::{format_ident, quote};
use syn::{ use syn::{
parenthesized, parenthesized,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
spanned::Spanned,
Fields, Token, Fields, Token,
}; };
@ -15,8 +16,9 @@ use crate::{
forward::WhichFn, forward::WhichFn,
}; };
pub struct Help { pub enum Help {
pub display: Display, Display(Display),
Field(syn::Member),
} }
impl Parse for Help { impl Parse for Help {
@ -38,16 +40,14 @@ impl Parse for Help {
args, args,
has_bonus_display: false, has_bonus_display: false,
}; };
Ok(Help { display }) Ok(Help::Display(display))
} else { } else {
input.parse::<Token![=]>()?; input.parse::<Token![=]>()?;
Ok(Help { Ok(Help::Display(Display {
display: Display { fmt: input.parse()?,
fmt: input.parse()?, args: TokenStream::new(),
args: TokenStream::new(), has_bonus_display: false,
has_bonus_display: false, }))
},
})
} }
} else { } else {
Err(syn::Error::new(ident.span(), "not a help")) Err(syn::Error::new(ident.span(), "not a help"))
@ -56,30 +56,83 @@ impl Parse for Help {
} }
impl Help { impl Help {
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("help") {
let help = 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(Help::Field(help)));
}
}
}
Ok(None)
}
pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> { pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
gen_all_variants_with( gen_all_variants_with(
variants, variants,
WhichFn::Help, WhichFn::Help,
|ident, fields, DiagnosticConcreteArgs { help, .. }| { |ident, fields, DiagnosticConcreteArgs { help, .. }| {
let (display_pat, display_members) = display_pat_members(fields); let (display_pat, display_members) = display_pat_members(fields);
let display = &help.as_ref()?.display; match &help.as_ref()? {
let (fmt, args) = display.expand_shorthand_cloned(&display_members); Help::Display(display) => {
Some(quote! { let (fmt, args) = display.expand_shorthand_cloned(&display_members);
Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), Some(quote! {
}) Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
})
}
Help::Field(member) => {
let help = match &member {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
Some(quote! {
Self::#ident #display_pat => #help.as_ref().map(|h| -> std::boxed::Box<dyn std::fmt::Display + 'a> { std::boxed::Box::new(format!("{}", h)) }),
})
}
}
}, },
) )
} }
pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> { pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> {
let (display_pat, display_members) = display_pat_members(fields); let (display_pat, display_members) = display_pat_members(fields);
let (fmt, args) = self.display.expand_shorthand_cloned(&display_members); match self {
Some(quote! { Help::Display(display) => {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> { let (fmt, args) = display.expand_shorthand_cloned(&display_members);
#[allow(unused_variables, deprecated)] Some(quote! {
let Self #display_pat = self; fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) #[allow(unused_variables, deprecated)]
let Self #display_pat = self;
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
}
})
} }
}) Help::Field(member) => Some(quote! {
fn help<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#[allow(unused_variables, deprecated)]
let Self #display_pat = self;
self.#member.as_ref().map(|h| -> std::boxed::Box<dyn std::fmt::Display + 'a> { std::boxed::Box::new(format!("{}", h)) })
}
}),
}
} }
} }

View File

@ -16,7 +16,7 @@ mod source_code;
mod url; mod url;
mod utils; mod utils;
#[proc_macro_derive(Diagnostic, attributes(diagnostic, source_code, label, related))] #[proc_macro_derive(Diagnostic, attributes(diagnostic, source_code, label, related, help))]
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) {

View File

@ -371,6 +371,42 @@
//! } //! }
//! ``` //! ```
//! //!
//! #### ... help text
//! `miette` provides two facilities for supplying help text for your errors:
//
//! The first is the `#[help()]` format attribute that applies to structs or
//! enum variants:
//!
//! ```rust
//! use miette::Diagnostic;
//! use thiserror::Error;
//!
//! #[derive(Debug, Diagnostic, Error)]
//! #[error("welp")]
//! #[diagnostic(help("try doing this instead"))]
//! struct Foo;
//! ```
//!
//! The other is by programmatically supplying the help text as a field to your
//! diagnostic:
//!
//! ```rust
//! use miette::Diagnostic;
//! use thiserror::Error;
//!
//! #[derive(Debug, Diagnostic, Error)]
//! #[error("welp")]
//! #[diagnostic()]
//! struct Foo {
//! #[help]
//! advice: Option<String>,
//! }
//!
//! let err = Foo {
//! advice: Some("try doing this instead".to_string()),
//! };
//! ```
//!
//! ### ... multiple related errors //! ### ... multiple related errors
//! //!
//! `miette` supports collecting multiple errors into a single diagnostic, and //! `miette` supports collecting multiple errors into a single diagnostic, and

View File

@ -238,6 +238,62 @@ fn fmt_help() {
); );
} }
#[test]
fn help_field() {
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Foo {
#[help]
do_this: Option<String>,
}
assert_eq!(
"x".to_string(),
Foo {
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
);
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
enum Bar {
A(#[help] Option<String>),
B {
#[help]
do_this: Option<String>,
},
}
assert_eq!(
"x".to_string(),
Bar::A(Some("x".into())).help().unwrap().to_string()
);
assert_eq!(
"x".to_string(),
Bar::B {
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
);
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Baz(#[help] Option<String>);
assert_eq!(
"x".to_string(),
Baz(Some("x".into())).help().unwrap().to_string()
);
}
#[test] #[test]
fn test_snippet_named_struct() { fn test_snippet_named_struct() {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]