error-path-macros 0.1.0

Attribute macros for automatically attaching error paths to Result-returning Rust functions and impl blocks.
Documentation
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, ImplItem, ItemFn, ItemImpl, LitStr, ReturnType, Type};

/// Attach an error path to a single Result-returning function.
///
/// - `#[error_path]` uses the function name.
/// - `#[error_path("custom.path")]` uses the provided path.
#[proc_macro_attribute]
pub fn error_path(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);

    let path = if attr.is_empty() {
        input.sig.ident.to_string()
    } else {
        parse_macro_input!(attr as LitStr).value()
    };

    expand_fn(input, path).into()
}

/// Attach error paths to every Result-returning method in an impl block.
///
/// - `#[error_path_impl]` uses the trait name for trait impls, otherwise the type name.
/// - `#[error_path_impl("custom.prefix")]` uses the provided prefix.
/// - `#[error_path_skip]` can be placed on methods that should not be wrapped.
#[proc_macro_attribute]
pub fn error_path_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(item as ItemImpl);

    let base_path = if attr.is_empty() {
        infer_impl_name(&input)
    } else {
        parse_macro_input!(attr as LitStr).value()
    };

    for item in &mut input.items {
        if let ImplItem::Fn(method) = item {
            if take_skip_attr(&mut method.attrs) {
                continue;
            }

            if !returns_result(&method.sig.output) {
                continue;
            }

            let base_path_lit = LitStr::new(&base_path, method.sig.ident.span());
            let method_path_lit =
                LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span());
            let block = &method.block;

            if method.sig.asyncness.is_some() {
                method.block = syn::parse_quote!({
                    let __error_path_result = (async #block).await;
                    __error_path_result.map_err(|e| {
                        let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
                        ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
                    })
                });
            } else {
                method.block = syn::parse_quote!({
                    let __error_path_result = (|| #block)();
                    __error_path_result.map_err(|e| {
                        let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
                        ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
                    })
                });
            }
        }
    }

    quote!(#input).into()
}

/// Marker attribute for methods inside `#[error_path_impl]` blocks.
///
/// This attribute is consumed by `#[error_path_impl]`.
#[proc_macro_attribute]
pub fn error_path_skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
    item
}

fn expand_fn(input: ItemFn, path: String) -> proc_macro2::TokenStream {
    let attrs = input.attrs;
    let vis = input.vis;
    let sig = input.sig;
    let block = input.block;
    let path_lit = LitStr::new(&path, sig.ident.span());

    if sig.asyncness.is_some() {
        quote! {
            #(#attrs)*
            #vis #sig {
                let __error_path_result = (async #block).await;
                __error_path_result.map_err(|e| {
                    ::error_path::WithErrorPath::with_error_path(e, #path_lit)
                })
            }
        }
    } else {
        quote! {
            #(#attrs)*
            #vis #sig {
                let __error_path_result = (|| #block)();
                __error_path_result.map_err(|e| {
                    ::error_path::WithErrorPath::with_error_path(e, #path_lit)
                })
            }
        }
    }
}

fn returns_result(output: &ReturnType) -> bool {
    let ReturnType::Type(_, output_type) = output else {
        return false;
    };
    let Type::Path(type_path) = output_type.as_ref() else {
        return false;
    };

    type_path
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident.to_string().ends_with("Result"))
}

fn infer_impl_name(input: &ItemImpl) -> String {
    if let Some((_, trait_path, _)) = &input.trait_ {
        return trait_path
            .segments
            .last()
            .map(|seg| seg.ident.to_string())
            .unwrap_or_else(|| "impl".to_string());
    }

    input.self_ty.to_token_stream().to_string().replace(' ', "")
}

fn take_skip_attr(attrs: &mut Vec<syn::Attribute>) -> bool {
    let before = attrs.len();
    attrs.retain(|attr| {
        attr.path()
            .segments
            .last()
            .map(|seg| seg.ident != "error_path_skip")
            .unwrap_or(true)
    });
    attrs.len() != before
}