1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{parse_macro_input, ImplItem, ItemFn, ItemImpl, LitStr, ReturnType, Type};
4
5#[proc_macro_attribute]
10pub fn error_path(attr: TokenStream, item: TokenStream) -> TokenStream {
11 let input = parse_macro_input!(item as ItemFn);
12
13 let path = if attr.is_empty() {
14 input.sig.ident.to_string()
15 } else {
16 parse_macro_input!(attr as LitStr).value()
17 };
18
19 expand_fn(input, path).into()
20}
21
22#[proc_macro_attribute]
28pub fn error_path_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
29 let mut input = parse_macro_input!(item as ItemImpl);
30
31 let base_path = if attr.is_empty() {
32 infer_impl_name(&input)
33 } else {
34 parse_macro_input!(attr as LitStr).value()
35 };
36
37 for item in &mut input.items {
38 if let ImplItem::Fn(method) = item {
39 if take_skip_attr(&mut method.attrs) {
40 continue;
41 }
42
43 if !returns_result(&method.sig.output) {
44 continue;
45 }
46
47 let base_path_lit = LitStr::new(&base_path, method.sig.ident.span());
48 let method_path_lit =
49 LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span());
50 let block = &method.block;
51
52 if method.sig.asyncness.is_some() {
53 method.block = syn::parse_quote!({
54 let __error_path_result = (async #block).await;
55 __error_path_result.map_err(|e| {
56 let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
57 ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
58 })
59 });
60 } else {
61 method.block = syn::parse_quote!({
62 let __error_path_result = (|| #block)();
63 __error_path_result.map_err(|e| {
64 let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
65 ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
66 })
67 });
68 }
69 }
70 }
71
72 quote!(#input).into()
73}
74
75#[proc_macro_attribute]
79pub fn error_path_skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
80 item
81}
82
83fn expand_fn(input: ItemFn, path: String) -> proc_macro2::TokenStream {
84 let attrs = input.attrs;
85 let vis = input.vis;
86 let sig = input.sig;
87 let block = input.block;
88 let path_lit = LitStr::new(&path, sig.ident.span());
89
90 if sig.asyncness.is_some() {
91 quote! {
92 #(#attrs)*
93 #vis #sig {
94 let __error_path_result = (async #block).await;
95 __error_path_result.map_err(|e| {
96 ::error_path::WithErrorPath::with_error_path(e, #path_lit)
97 })
98 }
99 }
100 } else {
101 quote! {
102 #(#attrs)*
103 #vis #sig {
104 let __error_path_result = (|| #block)();
105 __error_path_result.map_err(|e| {
106 ::error_path::WithErrorPath::with_error_path(e, #path_lit)
107 })
108 }
109 }
110 }
111}
112
113fn returns_result(output: &ReturnType) -> bool {
114 let ReturnType::Type(_, output_type) = output else {
115 return false;
116 };
117 let Type::Path(type_path) = output_type.as_ref() else {
118 return false;
119 };
120
121 type_path
122 .path
123 .segments
124 .last()
125 .is_some_and(|segment| segment.ident.to_string().ends_with("Result"))
126}
127
128fn infer_impl_name(input: &ItemImpl) -> String {
129 if let Some((_, trait_path, _)) = &input.trait_ {
130 return trait_path
131 .segments
132 .last()
133 .map(|seg| seg.ident.to_string())
134 .unwrap_or_else(|| "impl".to_string());
135 }
136
137 input.self_ty.to_token_stream().to_string().replace(' ', "")
138}
139
140fn take_skip_attr(attrs: &mut Vec<syn::Attribute>) -> bool {
141 let before = attrs.len();
142 attrs.retain(|attr| {
143 attr.path()
144 .segments
145 .last()
146 .map(|seg| seg.ident != "error_path_skip")
147 .unwrap_or(true)
148 });
149 attrs.len() != before
150}