wrap_match_impl/
lib.rs

1//! Please see <https://docs.rs/wrap-match>
2
3#![allow(
4    clippy::enum_glob_use,
5    clippy::match_bool,
6    clippy::if_not_else,
7    clippy::module_name_repetitions,
8    clippy::needless_pass_by_value,
9    clippy::implicit_clone
10)]
11
12use proc_macro::TokenStream;
13use quote::{format_ident, quote, quote_spanned, ToTokens};
14use syn::{
15    fold::Fold, parse_macro_input, parse_quote, spanned::Spanned, FnArg, ItemFn, Pat, ReturnType,
16    Type, Visibility,
17};
18
19mod add_error_info;
20use self::add_error_info::AddErrorInfo;
21
22mod options;
23use self::options::Options;
24
25mod log_statement;
26use self::log_statement::build_log_statement;
27
28#[proc_macro_attribute]
29#[allow(clippy::too_many_lines)]
30/// See crate level documentation for usage
31pub fn wrap_match(args: TokenStream, input: TokenStream) -> TokenStream {
32    let mut options = parse_macro_input!(args as Options);
33    let input = parse_macro_input!(input as ItemFn);
34
35    if match input.sig.output {
36        ReturnType::Default => true,
37        ReturnType::Type(_, ref ty) => match &**ty {
38            Type::Path(p) => p
39                .path
40                .segments
41                .last()
42                .map_or(true, |s| !s.ident.to_string().contains("Result")),
43            _ => true,
44        },
45    } {
46        let span = if let ReturnType::Type(_, t) = &input.sig.output {
47            t.span()
48        } else {
49            input.sig.span()
50        };
51        return quote_spanned! {span=>
52            compile_error!("wrap_match currently only supports functions that return `Result`s");
53        }
54        .into();
55    }
56
57    if let Some(constness) = &input.sig.constness {
58        return quote_spanned! {constness.span()=>
59            compile_error!("wrap_match cannot be used on const functions because the log crate cannot be used in const contexts");
60        }
61        .into();
62    }
63
64    let mut has_self_argument = false;
65    // remove types from args for use when calling the inner function
66    let mut args_without_types = vec![];
67    let mut args_without_types_including_self = vec![];
68    for arg in &input.sig.inputs {
69        match arg {
70            FnArg::Receiver(_) => {
71                has_self_argument = true;
72                args_without_types_including_self.push(quote!(self));
73            }
74            FnArg::Typed(arg) => {
75                let tokens = if let Pat::Ident(mut a) = *arg.pat.clone() {
76                    a.attrs.clear();
77                    a.mutability = None;
78                    a.into_token_stream()
79                } else {
80                    arg.pat.clone().into_token_stream()
81                };
82                args_without_types.push(tokens.clone());
83                args_without_types_including_self.push(tokens);
84            }
85        }
86    }
87
88    let self_dot = if has_self_argument {
89        quote!(self.)
90    } else {
91        quote!()
92    };
93
94    let asyncness_await = match input.sig.asyncness {
95        Some(_) => quote!(.await),
96        None => quote!(),
97    };
98
99    let attrs = input.attrs.clone();
100    let vis = input.vis.clone();
101    let mut sig = input.sig.clone();
102    if options.disregard_result {
103        sig.output = ReturnType::Default;
104    }
105
106    let orig_name = input.sig.ident.clone();
107    options.replace_function_in_messages(orig_name.to_string());
108    let inner_name = format_ident!("_wrap_match_inner_{}", orig_name);
109
110    let mut input = AddErrorInfo.fold_item_fn(input);
111    input.sig.ident = inner_name.clone();
112    input.vis = Visibility::Inherited; // make sure the inner function isn't leaked to the public
113    input.attrs = vec![
114        // we will put the original attributes on the function we make
115        // we also don't want the inner function to appear in docs or autocomplete (if they do, they should be deprecated and give a warning if they are used)
116        parse_quote!(#[doc(hidden)]),
117        parse_quote!(#[deprecated = "inner function for wrap-match. Please do not use!"]),
118        parse_quote!(#[inline(always)]), // let's make sure we don't produce more overhead than we need to, the output should produce similar assembly to the input (besides the end)
119    ];
120
121    let log_success = if options.log_success {
122        Some(build_log_statement(
123            &options.success_message,
124            &[],
125            &args_without_types_including_self,
126            quote!(info),
127        ))
128    } else {
129        None
130    };
131
132    let log_error = build_log_statement(
133        &options.error_message,
134        &[
135            ("line", quote!(_line)),
136            ("expr", quote!(_expr)),
137            ("error", quote!(e.inner)),
138        ],
139        &args_without_types_including_self,
140        quote!(error),
141    );
142
143    let log_error_without_info = build_log_statement(
144        &options.error_message_without_info,
145        &[("error", quote!(e.inner))],
146        &args_without_types_including_self,
147        quote!(error),
148    );
149
150    let ok = if !options.disregard_result {
151        quote!(Ok(r))
152    } else {
153        quote!()
154    };
155    let err = if !options.disregard_result {
156        quote!(Err(e.inner))
157    } else {
158        quote!()
159    };
160
161    // for functions that take a self argument, we will need to put the inner function outside of our new function since we don't know what type self is
162    let (outer_input, inner_input) = if has_self_argument {
163        (Some(input), None)
164    } else {
165        (None, Some(input))
166    };
167
168    quote! {
169        #outer_input
170
171        #(#attrs)* #vis #sig {
172            #inner_input
173
174            #[allow(deprecated)]
175            match #self_dot #inner_name(#(#args_without_types),*) #asyncness_await {
176                Ok(r) => {
177                    #log_success
178                    #ok
179                }
180                Err(e) => {
181                    if let Some((_line, _expr)) = e.line_and_expr {
182                        #log_error
183                    } else {
184                        #log_error_without_info
185                    }
186                    #err
187                }
188            }
189        }
190    }
191    .into()
192}