Repository: zkat/miette
Pull request #152 (2022-04-18 01:06:41)

feat(help): update macro to allow optional help text
Fixes: https://github.com/zkat/miette/issues/148


Patch:
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<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
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::<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"))
@@ -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)) })
+                }
+            }),
+        }
     }
 }
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<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


Test patch:
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<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)]