axum_debug/
lib.rs

1//! This is a debugging crate that provides better error messages for [`axum`] framework.
2//!
3//! **Note:** this crate is deprecated. Use [axum-macros] instead.
4//!
5//! [axum-macros]: https://crates.io/crates/axum-macros
6//!
7//! [`axum`]: https://docs.rs/axum/latest
8
9use proc_macro::TokenStream;
10
11/// Generates better error messages when applied to a handler function.
12///
13/// Note this crate is deprecated. Use [axum-macros] instead.
14///
15/// [axum-macros]: https://crates.io/crates/axum-macros
16#[deprecated(since = "0.3.3", note = "Use the axum-macros crate instead")]
17#[proc_macro_attribute]
18pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
19    #[cfg(not(debug_assertions))]
20    return input;
21
22    #[cfg(debug_assertions)]
23    return debug_handler::expand(_attr, input);
24}
25
26#[cfg(debug_assertions)]
27mod debug_handler {
28    use proc_macro2::TokenStream;
29    use quote::{format_ident, quote, quote_spanned};
30    use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, Token, Type};
31
32    pub(crate) fn expand(
33        attr: proc_macro::TokenStream,
34        input: proc_macro::TokenStream,
35    ) -> proc_macro::TokenStream {
36        match try_expand(attr.into(), input.into()) {
37            Ok(tokens) => tokens.into(),
38            Err(err) => err.into_compile_error().into(),
39        }
40    }
41
42    pub(crate) fn try_expand(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
43        let attr = syn::parse2::<Attrs>(attr)?;
44        let item_fn = syn::parse2::<ItemFn>(input.clone())?;
45
46        check_extractor_count(&item_fn)?;
47
48        let check_inputs_impls_from_request =
49            check_inputs_impls_from_request(&item_fn, &attr.body_ty);
50        let check_output_impls_into_response = check_output_impls_into_response(&item_fn);
51        let check_future_send = check_future_send(&item_fn);
52
53        let tokens = quote! {
54            #input
55            #check_inputs_impls_from_request
56            #check_output_impls_into_response
57            #check_future_send
58        };
59
60        Ok(tokens)
61    }
62
63    struct Attrs {
64        body_ty: Type,
65    }
66
67    impl Parse for Attrs {
68        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
69            let mut body_ty = None;
70
71            while !input.is_empty() {
72                let ident = input.parse::<syn::Ident>()?;
73                if ident == "body" {
74                    input.parse::<Token![=]>()?;
75                    body_ty = Some(input.parse()?);
76                } else {
77                    return Err(syn::Error::new_spanned(ident, "unknown argument"));
78                }
79
80                let _ = input.parse::<Token![,]>();
81            }
82
83            let body_ty = body_ty.unwrap_or_else(|| syn::parse_quote!(axum::body::Body));
84
85            Ok(Self { body_ty })
86        }
87    }
88
89    fn check_extractor_count(item_fn: &ItemFn) -> syn::Result<()> {
90        let max_extractors = 16;
91        if item_fn.sig.inputs.len() <= max_extractors {
92            Ok(())
93        } else {
94            Err(syn::Error::new_spanned(
95                &item_fn.sig.inputs,
96                format!(
97                    "Handlers cannot take more than {} arguments. Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
98                    max_extractors,
99                )
100            ))
101        }
102    }
103
104    fn check_inputs_impls_from_request(item_fn: &ItemFn, body_ty: &Type) -> TokenStream {
105        if !item_fn.sig.generics.params.is_empty() {
106            return syn::Error::new_spanned(
107                &item_fn.sig.generics,
108                "`#[axum_debug::debug_handler]` doesn't support generic functions",
109            )
110            .into_compile_error();
111        }
112
113        item_fn
114            .sig
115            .inputs
116            .iter()
117            .enumerate()
118            .map(|(idx, arg)| {
119                let (span, ty) = match arg {
120                    FnArg::Receiver(receiver) => {
121                        if receiver.reference.is_some() {
122                            return syn::Error::new_spanned(
123                                receiver,
124                                "Handlers must only take owned values",
125                            )
126                            .into_compile_error();
127                        }
128
129                        let span = receiver.span();
130                        (span, syn::parse_quote!(Self))
131                    }
132                    FnArg::Typed(typed) => {
133                        let ty = &typed.ty;
134                        let span = ty.span();
135                        (span, ty.clone())
136                    }
137                };
138
139                let name = format_ident!(
140                    "__axum_debug_check_{}_{}_from_request",
141                    item_fn.sig.ident,
142                    idx
143                );
144                quote_spanned! {span=>
145                    #[allow(warnings)]
146                    fn #name()
147                    where
148                        #ty: ::axum::extract::FromRequest<#body_ty> + Send,
149                    {}
150                }
151            })
152            .collect::<TokenStream>()
153    }
154
155    fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
156        let ty = match &item_fn.sig.output {
157            syn::ReturnType::Default => return quote! {},
158            syn::ReturnType::Type(_, ty) => ty,
159        };
160        let span = ty.span();
161
162        let declare_inputs = item_fn
163            .sig
164            .inputs
165            .iter()
166            .filter_map(|arg| match arg {
167                FnArg::Receiver(_) => None,
168                FnArg::Typed(pat_ty) => {
169                    let pat = &pat_ty.pat;
170                    let ty = &pat_ty.ty;
171                    Some(quote! {
172                        let #pat: #ty = panic!();
173                    })
174                }
175            })
176            .collect::<TokenStream>();
177
178        let block = &item_fn.block;
179
180        let make_value_name = format_ident!(
181            "__axum_debug_check_{}_into_response_make_value",
182            item_fn.sig.ident
183        );
184
185        let make = if item_fn.sig.asyncness.is_some() {
186            quote_spanned! {span=>
187                #[allow(warnings)]
188                async fn #make_value_name() -> #ty {
189                    #declare_inputs
190                    #block
191                }
192            }
193        } else {
194            quote_spanned! {span=>
195                #[allow(warnings)]
196                fn #make_value_name() -> #ty {
197                    #declare_inputs
198                    #block
199                }
200            }
201        };
202
203        let name = format_ident!("__axum_debug_check_{}_into_response", item_fn.sig.ident);
204
205        if let Some(receiver) = self_receiver(item_fn) {
206            quote_spanned! {span=>
207                #make
208
209                #[allow(warnings)]
210                async fn #name() {
211                    let value = #receiver #make_value_name().await;
212                    fn check<T>(_: T)
213                        where T: ::axum::response::IntoResponse
214                    {}
215                    check(value);
216                }
217            }
218        } else {
219            quote_spanned! {span=>
220                #[allow(warnings)]
221                async fn #name() {
222                    #make
223
224                    let value = #make_value_name().await;
225
226                    fn check<T>(_: T)
227                    where T: ::axum::response::IntoResponse
228                    {}
229
230                    check(value);
231                }
232            }
233        }
234    }
235
236    fn check_future_send(item_fn: &ItemFn) -> TokenStream {
237        if item_fn.sig.asyncness.is_none() {
238            match &item_fn.sig.output {
239                syn::ReturnType::Default => {
240                    return syn::Error::new_spanned(
241                        &item_fn.sig.fn_token,
242                        "Handlers must be `async fn`s",
243                    )
244                    .into_compile_error();
245                }
246                syn::ReturnType::Type(_, ty) => ty,
247            };
248        }
249
250        let span = item_fn.span();
251
252        let handler_name = &item_fn.sig.ident;
253
254        let args = item_fn.sig.inputs.iter().map(|_| {
255            quote_spanned! {span=> panic!() }
256        });
257
258        let name = format_ident!("__axum_debug_check_{}_future", item_fn.sig.ident);
259
260        if let Some(receiver) = self_receiver(item_fn) {
261            quote_spanned! {span=>
262                #[allow(warnings)]
263                fn #name() {
264                    let future = #receiver #handler_name(#(#args),*);
265                    fn check<T>(_: T)
266                        where T: ::std::future::Future + Send
267                    {}
268                    check(future);
269                }
270            }
271        } else {
272            quote_spanned! {span=>
273                #[allow(warnings)]
274                fn #name() {
275                    #item_fn
276
277                    let future = #handler_name(#(#args),*);
278                    fn check<T>(_: T)
279                        where T: ::std::future::Future + Send
280                    {}
281                    check(future);
282                }
283            }
284        }
285    }
286
287    fn self_receiver(item_fn: &ItemFn) -> Option<TokenStream> {
288        let takes_self = item_fn
289            .sig
290            .inputs
291            .iter()
292            .any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
293        if takes_self {
294            return Some(quote! { Self:: });
295        }
296
297        if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output {
298            if let syn::Type::Path(path) = &**ty {
299                let segments = &path.path.segments;
300                if segments.len() == 1 {
301                    if let Some(last) = segments.last() {
302                        match &last.arguments {
303                            syn::PathArguments::None if last.ident == "Self" => {
304                                return Some(quote! { Self:: });
305                            }
306                            _ => {}
307                        }
308                    }
309                }
310            }
311        }
312
313        None
314    }
315}