diff --git a/Makefile.toml b/Makefile.toml index 1b83b43..81df573 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -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"] diff --git a/README.md b/README.md index a55d9b1..0f27314 100644 --- a/README.md +++ b/README.md @@ -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 +} + +let err = Foo { advice: Some("try doing this instead".to_string()) }; +``` + #### ... multiple related errors `miette` supports collecting multiple errors into a single diagnostic, and diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index 0d57841..ef1bdae 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -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, diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs index c8879ca..1bbd51e 100644 --- a/miette-derive/src/help.rs +++ b/miette-derive/src/help.rs @@ -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, }; @@ -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 { @@ -38,16 +40,14 @@ impl Parse for Help { args, has_bonus_display: false, }; - Ok(Help { display }) + Ok(Help::Display(display)) } else { input.parse::()?; - 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")) @@ -56,30 +56,83 @@ impl Parse for Help { } impl Help { + 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("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 { 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 { std::boxed::Box::new(format!("{}", h)) }), + }) + } + } }, ) } pub(crate) fn gen_struct(&self, fields: &Fields) -> Option { 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> { - #[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> { + #[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> { + #[allow(unused_variables, deprecated)] + let Self #display_pat = self; + self.#member.as_ref().map(|h| -> std::boxed::Box { std::boxed::Box::new(format!("{}", h)) }) + } + }), + } } } diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 1672c4b..da8f8bb 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -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) { diff --git a/src/lib.rs b/src/lib.rs index d52a8f9..e40b7d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, +//! } +//! +//! let err = Foo { +//! advice: Some("try doing this instead".to_string()), +//! }; +//! ``` +//! //! ### ... multiple related errors //! //! `miette` supports collecting multiple errors into a single diagnostic, and diff --git a/tests/derive.rs b/tests/derive.rs index 60a7763..e269621 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -238,6 +238,62 @@ fn fmt_help() { ); } +#[test] +fn help_field() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic()] + struct Foo { + #[help] + do_this: Option, + } + + 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), + B { + #[help] + do_this: Option, + }, + } + + 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); + + assert_eq!( + "x".to_string(), + Baz(Some("x".into())).help().unwrap().to_string() + ); +} + #[test] fn test_snippet_named_struct() { #[derive(Debug, Diagnostic, Error)]