Skip to content

Commit

Permalink
feat(help): update macro to allow optional help text (#152)
Browse files Browse the repository at this point in the history
Fixes: #148
  • Loading branch information
zkat authored Apr 18, 2022
1 parent 5e54b29 commit 45093c2
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 25 deletions.
6 changes: 6 additions & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ workspace=false
install_crate="cargo-release"
command = "cargo"
args = ["release", "--workspace", "${@}"]

[tasks.readme]
workspace=false
install_crate="cargo-readme"
command = "cargo"
args = ["readme"]
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

`miette` supports collecting multiple errors into a single diagnostic, and
Expand Down
3 changes: 2 additions & 1 deletion miette-derive/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ impl DiagnosticConcreteArgs {
let labels = Labels::from_fields(fields)?;
let source_code = SourceCode::from_fields(fields)?;
let related = Related::from_fields(fields)?;
let help = Help::from_fields(fields)?;
Ok(DiagnosticConcreteArgs {
code: None,
help: None,
help,
related,
severity: None,
labels,
Expand Down
99 changes: 76 additions & 23 deletions miette-derive/src/help.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use proc_macro2::TokenStream;
use quote::quote;
use quote::{format_ident, quote};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
spanned::Spanned,
Fields, Token,
};

Expand All @@ -15,8 +16,9 @@ use crate::{
forward::WhichFn,
};

pub struct Help {
pub display: Display,
pub enum Help {
Display(Display),
Field(syn::Member),
}

impl Parse for Help {
Expand All @@ -38,16 +40,14 @@ impl Parse for Help {
args,
has_bonus_display: false,
};
Ok(Help { display })
Ok(Help::Display(display))
} else {
input.parse::<Token![=]>()?;
Ok(Help {
display: Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
},
})
Ok(Help::Display(Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
}))
}
} else {
Err(syn::Error::new(ident.span(), "not a help"))
Expand All @@ -56,30 +56,83 @@ impl Parse for 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> {
gen_all_variants_with(
variants,
WhichFn::Help,
|ident, fields, DiagnosticConcreteArgs { help, .. }| {
let (display_pat, display_members) = display_pat_members(fields);
let display = &help.as_ref()?.display;
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
Some(quote! {
Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
})
match &help.as_ref()? {
Help::Display(display) => {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
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> {
let (display_pat, display_members) = display_pat_members(fields);
let (fmt, args) = self.display.expand_shorthand_cloned(&display_members);
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;
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
match self {
Help::Display(display) => {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
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;
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)) })
}
}),
}
}
}
2 changes: 1 addition & 1 deletion miette-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ mod source_code;
mod url;
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 {
let input = parse_macro_input!(input as DeriveInput);
let cmd = match Diagnostic::from_derive_input(input) {
Expand Down
36 changes: 36 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
//! `miette` supports collecting multiple errors into a single diagnostic, and
Expand Down
56 changes: 56 additions & 0 deletions tests/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
fn test_snippet_named_struct() {
#[derive(Debug, Diagnostic, Error)]
Expand Down

0 comments on commit 45093c2

Please sign in to comment.