Make axum-debug handle more cases (#518)

* Make `axum-debug` handle more cases

* Only just trybuild tests on stable

* revert changes to hello-world example

* Remove a bit of duplication

* return error on generics

* address review feedback

* Support associated functions with receiver or returns `Self`

* fix indentation
This commit is contained in:
David Pedersen 2021-11-19 21:32:07 +01:00 committed by GitHub
parent 22931688f7
commit f1f004a057
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 408 additions and 275 deletions

View file

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased # Unreleased
- Make macro handle more cases such as mutable extractors and handlers taking
`self` ([#518])
[#518]: https://github.com/tokio-rs/axum/pull/518
# 0.2.0 (13. October 2021) # 0.2.0 (13. October 2021)
- **breaking:** Removed `debug_router` macro. - **breaking:** Removed `debug_router` macro.

View file

@ -21,3 +21,4 @@ syn = { version = "1.0", features = ["full"] }
[dev-dependencies] [dev-dependencies]
axum = { path = "../axum", version = "0.3" } axum = { path = "../axum", version = "0.3" }
trybuild = "1.0" trybuild = "1.0"
rustversion = "1.0"

View file

@ -124,298 +124,273 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
/// Generates better error messages when applied to a handler function. /// Generates better error messages when applied to a handler function.
///
/// # Examples
///
/// Function is not async:
///
/// ```rust,compile_fail
/// #[axum_debug::debug_handler]
/// fn handler() -> &'static str {
/// "Hello, world"
/// }
/// ```
///
/// ```text
/// error: handlers must be async functions
/// --> main.rs:xx:1
/// |
/// xx | fn handler() -> &'static str {
/// | ^^
/// ```
///
/// Wrong return type:
///
/// ```rust,compile_fail
/// #[axum_debug::debug_handler]
/// async fn handler() -> bool {
/// false
/// }
/// ```
///
/// ```text
/// error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
/// --> main.rs:xx:23
/// |
/// xx | async fn handler() -> bool {
/// | ^^^^
/// | |
/// | the trait `IntoResponse` is not implemented for `bool`
/// ```
///
/// Wrong extractor:
///
/// ```rust,compile_fail
/// #[axum_debug::debug_handler]
/// async fn handler(a: bool) -> String {
/// format!("Can I extract a bool? {}", a)
/// }
/// ```
///
/// ```text
/// error[E0277]: the trait bound `bool: FromRequest` is not satisfied
/// --> main.rs:xx:21
/// |
/// xx | async fn handler(a: bool) -> String {
/// | ^^^^
/// | |
/// | the trait `FromRequest` is not implemented for `bool`
/// ```
///
/// Too many extractors:
///
/// ```rust,compile_fail
/// #[axum_debug::debug_handler]
/// async fn handler(
/// a: String,
/// b: String,
/// c: String,
/// d: String,
/// e: String,
/// f: String,
/// g: String,
/// h: String,
/// i: String,
/// j: String,
/// k: String,
/// l: String,
/// m: String,
/// n: String,
/// o: String,
/// p: String,
/// q: String,
/// ) {}
/// ```
///
/// ```text
/// error: too many extractors. 16 extractors are allowed
/// note: you can nest extractors like "a: (Extractor, Extractor), b: (Extractor, Extractor)"
/// --> main.rs:xx:5
/// |
/// xx | / a: String,
/// xx | | b: String,
/// xx | | c: String,
/// xx | | d: String,
/// ... |
/// xx | | p: String,
/// xx | | q: String,
/// | |______________^
/// ```
///
/// Future is not [`Send`]:
///
/// ```rust,compile_fail
/// #[axum_debug::debug_handler]
/// async fn handler() {
/// let not_send = std::rc::Rc::new(());
///
/// async {}.await;
/// }
/// ```
///
/// ```text
/// error: future cannot be sent between threads safely
/// --> main.rs:xx:10
/// |
/// xx | async fn handler() {
/// | ^^^^^^^
/// | |
/// | future returned by `handler` is not `Send`
/// ```
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream { pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
return input; return input;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
return debug::apply_debug_handler(input); return debug_handler::expand(_attr, input);
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
mod debug { mod debug_handler {
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote_spanned}; use quote::{format_ident, quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, FnArg, Ident, ItemFn, ReturnType, Signature}; use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn};
pub(crate) fn apply_debug_handler(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub(crate) fn expand(
let function = parse_macro_input!(input as ItemFn); attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
let vis = &function.vis; ) -> proc_macro::TokenStream {
let sig = &function.sig; match try_expand(attr.into(), input.into()) {
let ident = &sig.ident; Ok(tokens) => tokens.into(),
let span = ident.span(); Err(err) => err.into_compile_error().into(),
let len = sig.inputs.len();
let generics = create_generics(len);
let params = sig.inputs.iter().map(|fn_arg| {
if let FnArg::Typed(pat_type) = fn_arg {
&pat_type.pat
} else {
panic!("not a handler function");
}
});
let block = &function.block;
if let Err(err) = async_check(sig) {
return err.into_compile_error().into();
} }
}
if let Err(err) = param_limit_check(sig) { pub(crate) fn try_expand(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
return err.into_compile_error().into(); syn::parse2::<Attrs>(attr)?;
} let item_fn = syn::parse2::<ItemFn>(input.clone())?;
let check_trait = check_trait_code(sig, &generics); check_extractor_count(&item_fn)?;
let check_return = check_return_code(sig, &generics);
let check_params = match check_params_code(sig, &generics) { let check_inputs_impls_from_request = check_inputs_impls_from_request(&item_fn);
Ok(tokens) => tokens, let check_output_impls_into_response = check_output_impls_into_response(&item_fn);
Err(err) => return err.into_compile_error().into(), let check_future_send = check_future_send(&item_fn);
let tokens = quote! {
#input
#check_inputs_impls_from_request
#check_output_impls_into_response
#check_future_send
}; };
let expanded = quote_spanned! {span=> Ok(tokens)
#vis #sig {
#check_trait
#check_return
#(#check_params)*
#sig #block
#ident(#(#params),*).await
}
};
expanded.into()
} }
fn create_generics(len: usize) -> Vec<Ident> { struct Attrs;
(1..=len).map(|i| format_ident!("T{}", i)).collect()
}
fn async_check(sig: &Signature) -> Result<(), syn::Error> { impl Parse for Attrs {
if sig.asyncness.is_none() { fn parse(_input: syn::parse::ParseStream) -> syn::Result<Self> {
Err(syn::Error::new_spanned( Ok(Self)
sig.fn_token,
"handlers must be async functions",
))
} else {
Ok(())
} }
} }
fn param_limit_check(sig: &Signature) -> Result<(), syn::Error> { fn check_extractor_count(item_fn: &ItemFn) -> syn::Result<()> {
let max_extractors = 16; let max_extractors = 16;
if item_fn.sig.inputs.len() <= max_extractors {
if sig.inputs.len() > max_extractors {
let msg = format!(
"too many extractors. {} extractors are allowed\n\
note: you can nest extractors like \"a: (Extractor, Extractor), b: (Extractor, Extractor)\"",
max_extractors
);
Err(syn::Error::new_spanned(&sig.inputs, msg))
} else {
Ok(()) Ok(())
} else {
Err(syn::Error::new_spanned(
&item_fn.sig.inputs,
format!(
"Handlers cannot take more than {} arguments. Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
max_extractors,
)
))
} }
} }
fn check_trait_code(sig: &Signature, generics: &[Ident]) -> proc_macro2::TokenStream { fn check_inputs_impls_from_request(item_fn: &ItemFn) -> TokenStream {
let ident = &sig.ident; if !item_fn.sig.generics.params.is_empty() {
let span = ident.span(); return syn::Error::new_spanned(
&item_fn.sig.generics,
quote_spanned! {span=> "`#[axum_debug::debug_handler]` doesn't support generic functions",
{ )
debug_handler(#ident); .into_compile_error();
fn debug_handler<F, Fut, #(#generics),*>(_f: F)
where
F: ::std::ops::FnOnce(#(#generics),*) -> Fut + Clone + Send + 'static,
Fut: ::std::future::Future + Send,
{}
}
} }
}
fn check_return_code(sig: &Signature, generics: &[Ident]) -> proc_macro2::TokenStream { item_fn
let span = match &sig.output { .sig
ReturnType::Default => sig.output.span(), .inputs
ReturnType::Type(_, ty) => ty.span(),
};
let ident = &sig.ident;
quote_spanned! {span=>
{
debug_handler(#ident);
fn debug_handler<F, Fut, Res, #(#generics),*>(_f: F)
where
F: ::std::ops::FnOnce(#(#generics),*) -> Fut,
Fut: ::std::future::Future<Output = Res>,
Res: ::axum::response::IntoResponse,
{}
}
}
}
fn check_params_code(
sig: &Signature,
generics: &[Ident],
) -> Result<Vec<TokenStream>, syn::Error> {
let ident = &sig.ident;
generics
.iter() .iter()
.enumerate() .map(|arg| {
.map(|(i, generic)| { let (span, ty) = match arg {
let span = match &sig.inputs[i] {
FnArg::Typed(pat_type) => pat_type.ty.span(),
FnArg::Receiver(receiver) => { FnArg::Receiver(receiver) => {
// TODO: look into whether its possible to make this work if receiver.reference.is_some() {
return Err(syn::Error::new_spanned( return syn::Error::new_spanned(
receiver, receiver,
"`#[debug_handler]` is not supported on methods", "Handlers must only take owned values",
)); )
.into_compile_error();
}
let span = receiver.span();
(span, syn::parse_quote!(Self))
}
FnArg::Typed(typed) => {
let ty = &typed.ty;
let span = ty.span();
(span, ty.clone())
} }
}; };
let tokens = quote_spanned! {span=> let name = format_ident!("__axum_debug_check_{}_from_request", item_fn.sig.ident);
{ quote_spanned! {span=>
debug_handler(#ident); #[allow(warnings)]
fn #name()
fn debug_handler<F, Fut, #(#generics),*>(_f: F) where
where #ty: ::axum::extract::FromRequest + Send,
F: ::std::ops::FnOnce(#(#generics),*) -> Fut, {}
Fut: ::std::future::Future, }
#generic: ::axum::extract::FromRequest + Send,
{}
}
};
Ok(tokens)
}) })
.collect() .collect::<TokenStream>()
}
fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
let ty = match &item_fn.sig.output {
syn::ReturnType::Default => return quote! {},
syn::ReturnType::Type(_, ty) => ty,
};
let span = ty.span();
let make_value_name = format_ident!(
"__axum_debug_check_{}_into_response_make_value",
item_fn.sig.ident
);
let make = if item_fn.sig.asyncness.is_some() {
quote_spanned! {span=>
#[allow(warnings)]
async fn #make_value_name() -> #ty { panic!() }
}
} else if let syn::Type::ImplTrait(_) = &**ty {
// lets just assume it returns `impl Future`
quote_spanned! {span=>
#[allow(warnings)]
fn #make_value_name() -> #ty { async { panic!() } }
}
} else {
quote_spanned! {span=>
#[allow(warnings)]
fn #make_value_name() -> #ty { panic!() }
}
};
let name = format_ident!("__axum_debug_check_{}_into_response", item_fn.sig.ident);
if let Some(receiver) = self_receiver(item_fn) {
quote_spanned! {span=>
#make
#[allow(warnings)]
async fn #name() {
let value = #receiver #make_value_name().await;
fn check<T>(_: T)
where T: ::axum::response::IntoResponse
{}
check(value);
}
}
} else {
quote_spanned! {span=>
#[allow(warnings)]
async fn #name() {
#make
let value = #make_value_name().await;
fn check<T>(_: T)
where T: ::axum::response::IntoResponse
{}
check(value);
}
}
}
}
fn check_future_send(item_fn: &ItemFn) -> TokenStream {
if item_fn.sig.asyncness.is_none() {
match &item_fn.sig.output {
syn::ReturnType::Default => {
return syn::Error::new_spanned(
&item_fn.sig.fn_token,
"Handlers must be `async fn`s",
)
.into_compile_error();
}
syn::ReturnType::Type(_, ty) => ty,
};
}
let span = item_fn.span();
let handler_name = &item_fn.sig.ident;
let args = item_fn.sig.inputs.iter().map(|_| {
quote_spanned! {span=> panic!() }
});
let name = format_ident!("__axum_debug_check_{}_future", item_fn.sig.ident);
if let Some(receiver) = self_receiver(item_fn) {
quote_spanned! {span=>
#[allow(warnings)]
fn #name() {
let future = #receiver #handler_name(#(#args),*);
fn check<T>(_: T)
where T: ::std::future::Future + Send
{}
check(future);
}
}
} else {
quote_spanned! {span=>
#[allow(warnings)]
fn #name() {
#item_fn
let future = #handler_name(#(#args),*);
fn check<T>(_: T)
where T: ::std::future::Future + Send
{}
check(future);
}
}
}
}
fn self_receiver(item_fn: &ItemFn) -> Option<TokenStream> {
let takes_self = item_fn
.sig
.inputs
.iter()
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
if takes_self {
return Some(quote! { Self:: });
}
if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output {
if let syn::Type::Path(path) = &**ty {
let segments = &path.path.segments;
if segments.len() == 1 {
if let Some(last) = segments.last() {
match &last.arguments {
syn::PathArguments::None if last.ident == "Self" => {
return Some(quote! { Self:: });
}
_ => {}
}
}
}
}
}
None
} }
} }
#[test] #[test]
fn ui() { fn ui() {
let t = trybuild::TestCases::new(); #[rustversion::stable]
t.pass("tests/pass/*.rs"); fn go() {
t.compile_fail("tests/fail/*.rs"); let t = trybuild::TestCases::new();
t.compile_fail("tests/fail/*.rs");
t.pass("tests/pass/*.rs");
}
#[rustversion::not(stable)]
fn go() {}
go();
} }

View file

@ -4,8 +4,4 @@ error[E0277]: the trait bound `bool: FromRequest` is not satisfied
4 | async fn handler(foo: bool) {} 4 | async fn handler(foo: bool) {}
| ^^^^ the trait `FromRequest` is not implemented for `bool` | ^^^^ the trait `FromRequest` is not implemented for `bool`
| |
note: required by a bound in `handler::{closure#0}::debug_handler` = help: see issue #48214
--> tests/fail/argument_not_extractor.rs:4:23
|
4 | async fn handler(foo: bool) {}
| ^^^^ required by this bound in `handler::{closure#0}::debug_handler`

View file

@ -0,0 +1,6 @@
use axum_debug::debug_handler;
#[debug_handler(foo)]
async fn handler() {}
fn main() {}

View file

@ -0,0 +1,5 @@
error: unexpected token
--> tests/fail/attrs.rs:3:17
|
3 | #[debug_handler(foo)]
| ^^^

View file

@ -0,0 +1,23 @@
use axum::{
async_trait,
extract::{FromRequest, RequestParts},
};
use axum_debug::debug_handler;
struct A;
#[async_trait]
impl FromRequest for A {
type Rejection = ();
async fn from_request(_req: &mut RequestParts) -> Result<Self, Self::Rejection> {
unimplemented!()
}
}
impl A {
#[debug_handler]
async fn handler(&mut self) {}
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: Handlers must only take owned values
--> tests/fail/extract_self_mut.rs:20:22
|
20 | async fn handler(&mut self) {}
| ^^^^^^^^^

View file

@ -0,0 +1,23 @@
use axum::{
async_trait,
extract::{FromRequest, RequestParts},
};
use axum_debug::debug_handler;
struct A;
#[async_trait]
impl FromRequest for A {
type Rejection = ();
async fn from_request(_req: &mut RequestParts) -> Result<Self, Self::Rejection> {
unimplemented!()
}
}
impl A {
#[debug_handler]
async fn handler(&self) {}
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: Handlers must only take owned values
--> tests/fail/extract_self_ref.rs:20:22
|
20 | async fn handler(&self) {}
| ^^^^^

View file

@ -0,0 +1,6 @@
use axum_debug::debug_handler;
#[debug_handler]
async fn handler<T>() {}
fn main() {}

View file

@ -0,0 +1,13 @@
error: `#[axum_debug::debug_handler]` doesn't support generic functions
--> tests/fail/generics.rs:4:17
|
4 | async fn handler<T>() {}
| ^^^
error[E0282]: type annotations needed
--> tests/fail/generics.rs:4:10
|
4 | async fn handler<T>() {}
| ----- ^^^^^^^ cannot infer type for type parameter `T` declared on the function `handler`
| |
| consider giving `future` a type

View file

@ -1,4 +1,4 @@
error: handlers must be async functions error: Handlers must be `async fn`s
--> tests/fail/not_async.rs:4:1 --> tests/fail/not_async.rs:4:1
| |
4 | fn handler() {} 4 | fn handler() {}

View file

@ -1,8 +1,8 @@
error: future cannot be sent between threads safely error: future cannot be sent between threads safely
--> tests/fail/not_send.rs:4:10 --> tests/fail/not_send.rs:4:1
| |
4 | async fn handler() { 4 | async fn handler() {
| ^^^^^^^ future returned by `handler` is not `Send` | ^^^^^ future returned by `handler` is not `Send`
| |
= help: within `impl Future`, the trait `Send` is not implemented for `Rc<()>` = help: within `impl Future`, the trait `Send` is not implemented for `Rc<()>`
note: future is not `Send` as this value is used across an await note: future is not `Send` as this value is used across an await
@ -14,8 +14,8 @@ note: future is not `Send` as this value is used across an await
| ^^^^^^^^^^^^^^ await occurs here, with `rc` maybe used later | ^^^^^^^^^^^^^^ await occurs here, with `rc` maybe used later
7 | } 7 | }
| - `rc` is later dropped here | - `rc` is later dropped here
note: required by a bound in `handler::{closure#0}::debug_handler` note: required by a bound in `check`
--> tests/fail/not_send.rs:4:10 --> tests/fail/not_send.rs:4:1
| |
4 | async fn handler() { 4 | async fn handler() {
| ^^^^^^^ required by this bound in `handler::{closure#0}::debug_handler` | ^^^^^ required by this bound in `check`

View file

@ -1,5 +0,0 @@
error: `#[debug_handler]` is not supported on methods
--> tests/fail/self_receiver.rs:17:22
|
17 | async fn handler(self) {}
| ^^^^

View file

@ -1,5 +1,4 @@
error: too many extractors. 16 extractors are allowed error: Handlers cannot take more than 16 arguments. Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors
note: you can nest extractors like "a: (Extractor, Extractor), b: (Extractor, Extractor)"
--> tests/fail/too_many_extractors.rs:5:5 --> tests/fail/too_many_extractors.rs:5:5
| |
5 | / e1: String, 5 | / e1: String,

View file

@ -4,8 +4,8 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
4 | async fn handler() -> bool { 4 | async fn handler() -> bool {
| ^^^^ the trait `IntoResponse` is not implemented for `bool` | ^^^^ the trait `IntoResponse` is not implemented for `bool`
| |
note: required by a bound in `handler::{closure#0}::debug_handler` note: required by a bound in `__axum_debug_check_handler_into_response::{closure#0}::check`
--> tests/fail/wrong_return_type.rs:4:23 --> tests/fail/wrong_return_type.rs:4:23
| |
4 | async fn handler() -> bool { 4 | async fn handler() -> bool {
| ^^^^ required by this bound in `handler::{closure#0}::debug_handler` | ^^^^ required by this bound in `__axum_debug_check_handler_into_response::{closure#0}::check`

View file

@ -0,0 +1,10 @@
use axum_debug::debug_handler;
struct A;
impl A {
#[debug_handler]
async fn handler() {}
}
fn main() {}

View file

@ -0,0 +1,9 @@
use axum_debug::debug_handler;
use std::future::Future;
#[debug_handler]
fn handler() -> impl Future<Output = ()> {
async {}
}
fn main() {}

View file

@ -0,0 +1,9 @@
use axum_debug::debug_handler;
use axum::response::IntoResponse;
#[debug_handler]
async fn handler() -> impl IntoResponse {
"hi!"
}
fn main() {}

View file

@ -0,0 +1,9 @@
use axum_debug::debug_handler;
#[debug_handler]
async fn handler(mut foo: String) -> String {
foo += "bar";
foo
}
fn main() {}

View file

@ -0,0 +1,9 @@
use axum_debug::debug_handler;
use std::future::{Ready, ready};
#[debug_handler]
fn handler() -> Ready<()> {
ready(())
}
fn main() {}

View file

@ -0,0 +1,27 @@
use axum::{
body::{Bytes, Full},
http::Response,
response::IntoResponse,
};
use axum_debug::debug_handler;
use std::convert::Infallible;
struct A;
impl A {
#[debug_handler]
async fn handler() -> Self {
A
}
}
impl IntoResponse for A {
type Body = Full<Bytes>;
type BodyError = Infallible;
fn into_response(self) -> Response<Self::Body> {
todo!()
}
}
fn main() {}

View file

@ -1,5 +1,8 @@
use axum::{
async_trait,
extract::{FromRequest, RequestParts},
};
use axum_debug::debug_handler; use axum_debug::debug_handler;
use axum::{async_trait, extract::{FromRequest, RequestParts}};
struct A; struct A;