error-doc-impl 0.1.0

A simple proc macro to generate #[doc] comments from #[error] messages
Documentation
use proc_macro2::TokenStream;
use quote::quote;

/// This macro derives `thiserror::Error` and [`Debug`] traits, and automatically generates missing documents for error variants from error messages.
///
/// # Example
///
/// ```
/// #[error_doc::errors]
/// pub enum SomeError {
///     #[error("failed to open config file")]
///     OpenFile(#[from] std::io::Error),
///     #[error(transparent)]
///     #[doc = "Database error"]
///     Database(#[from] sqlx::Error),
///     #[error("unexpected value: `{0}`")]
///     #[doc = "Unexpected value is provided"]
///     UnexpectedValue(u16),
///     #[error("some other error")]
///     Other,
/// }
/// ```
///
/// generates
///
/// ```
/// #[derive(thiserror::Error, Debug)]
/// pub enum SomeError {
///     #[error("failed to open config file")]
///     #[doc = "Failed to open config file"]
///     OpenFile(#[from] std::io::Error),
///     #[error(transparent)]
///     #[doc = "Database error"]
///     Database(#[from] sqlx::Error),
///     #[error("unexpected value: `{0}`")]
///     #[doc = "Unexpected value is provided"]
///     UnexpectedValue(u16),
///     #[error("some other error")]
///     #[doc = "Some other error"]
///     Other,
/// }
/// ```
#[proc_macro_attribute]
pub fn errors(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item: TokenStream = match error_doc_impl(item.into()) {
        Ok(token_stream) => token_stream.into(),
        Err(error) => return error.to_compile_error().into(),
    };

    quote! {
        #[derive(::core::fmt::Debug, ::error_doc::thiserror::Error)]
        #item
    }
    .into()
}

/// This macro automatically generates missing documents for error variants from error messages.
///
/// # Example
///
/// ```rust
/// #[error_doc::error_doc]
/// #[derive(thiserror::Error, Debug)]
/// pub enum SomeError {
///     #[error("failed to open config file")]
///     OpenFile(#[from] std::io::Error),
///     #[error(transparent)]
///     #[doc = "Database error"]
///     Database(#[from] sqlx::Error),
///     #[error("unexpected value: `{0}`")]
///     #[doc = "Unexpected value is provided"]
///     UnexpectedValue(u16),
///     #[error("some other error")]
///     Other,
/// }
/// ```
///
/// generates
///
/// ```rust
/// #[error_doc::error_doc]
/// #[derive(thiserror::Error, Debug)]
/// pub enum SomeError {
///     #[error("failed to open config file")]
///     #[doc = "Failed to open config file"]
///     OpenFile(#[from] std::io::Error),
///     #[error(transparent)]
///     #[doc = "Database error"]
///     Database(#[from] sqlx::Error),
///     #[error("unexpected value: `{0}`")]
///     #[doc = "Unexpected value is provided"]
///     UnexpectedValue(u16),
///     #[error("some other error")]
///     #[doc = "Some other error"]
///     Other,
/// }
/// ```
#[proc_macro_attribute]
pub fn error_doc(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    match error_doc_impl(item.into()) {
        Ok(token_stream) => token_stream.into(),
        Err(error) => error.to_compile_error().into(),
    }
}

fn error_doc_impl(item: TokenStream) -> syn::Result<TokenStream> {
    let mut item: syn::ItemEnum = syn::parse2(item)?;

    item.variants = item
        .variants
        .into_iter()
        .map(|mut variant| {
            // check if doc attribute is alredy there
            if variant.attrs.iter().any(|attr| attr.path().is_ident("doc")) {
                return variant;
            }

            let msg = variant.attrs.iter().find_map(|attr| {
                if !attr.path().is_ident("error") {
                    return None;
                }
                let lit: syn::LitStr = attr.parse_args().ok()?;
                let mut msg = lit.value();

                // capitalize the first letter
                if let Some(first_letter) = msg.get_mut(..1) {
                    first_letter.make_ascii_uppercase();
                }

                Some(msg)
            });

            // add #[doc] attribute
            if let Some(msg) = msg {
                variant.attrs.push(syn::parse_quote! { #[doc = #msg] });
            }

            variant
        })
        .collect();

    Ok(quote! { #item })
}