use proc_macro2::Ident;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::ToTokens;
use quote::quote;
use quote::quote_spanned;
use syn::Block;
use syn::Error;
use syn::Expr;
use syn::ExprAsync;
use syn::ExprCall;
use syn::Generics;
use syn::Item;
use syn::ItemFn;
use syn::LitStr;
use syn::Path;
use syn::Result;
use syn::ReturnType;
use syn::Signature;
use syn::Stmt;
use syn::Token;
use syn::Type;
use syn::TypeInfer;
use syn::parse_quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::visit_mut;
use syn::visit_mut::VisitMut;
use crate::args::Args;
pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result<TokenStream> {
let func_name = &input.sig.ident;
let func_body = if let Some(internal_fun) =
get_async_trait_info(&input.block, input.sig.asyncness.is_some())
{
match internal_fun.kind {
AsyncTraitKind::Function => {
unimplemented!(
"Please upgrade the crate `async-trait` to a version higher than 0.1.44"
)
}
AsyncTraitKind::Async(async_expr) => {
let instrumented_block =
gen_block(func_name, &async_expr.block, true, false, &args, None)?;
let async_attrs = &async_expr.attrs;
quote! {
Box::pin(#(#async_attrs) * #instrumented_block)
}
}
}
} else {
let output_ty = match &input.sig.output {
ReturnType::Type(_, ty) => (**ty).clone(),
ReturnType::Default => parse_quote! { () },
};
gen_block(
func_name,
&input.block,
input.sig.asyncness.is_some(),
input.sig.asyncness.is_some(),
&args,
Some(output_ty),
)?
};
let ItemFn {
attrs, vis, sig, ..
} = input;
let Signature {
output: return_type,
inputs: params,
unsafety,
constness,
abi,
ident,
asyncness,
generics:
Generics {
params: gen_params,
where_clause,
..
},
..
} = sig;
let fn_span = ident.span();
Ok(quote_spanned!(fn_span=>
#(#attrs) *
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
#func_body
}
))
}
fn gen_span_name(func_name: &Ident, args: &Args) -> Result<TokenStream> {
let Args {
name,
short_name,
crate_path,
..
} = args;
if let Some(span_name) = name {
if span_name.value().is_empty() {
Err(Error::new(Span::call_site(), "`name` can not be empty"))
} else if *short_name {
Err(Error::new(
Span::call_site(),
"`name` and `short_name` can not be used together",
))
} else {
Ok(name.into_token_stream())
}
} else {
if *short_name {
Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream())
} else {
Ok(quote!(#crate_path::func_path!()))
}
}
}
fn gen_properties(args: &Args) -> Result<TokenStream> {
if args.properties.is_empty() {
return Ok(quote!());
}
if args.enter_on_poll {
return Err(Error::new(
Span::call_site(),
"`enter_on_poll` can not be used with `properties`",
));
}
let properties = args.properties.iter().map(|p| {
let k = &p.key;
let v = &p.value;
quote!(
(std::borrow::Cow::from(#k), match format_args!(#v) {
__f => if let Some(__s) = __f.as_str() {
std::borrow::Cow::from(__s)
} else {
std::borrow::Cow::from(std::string::ToString::to_string(&__f))
}
})
)
});
let properties = Punctuated::<_, Token![,]>::from_iter(properties);
Ok(quote!(
.with_properties(|| [ #properties ])
))
}
fn gen_block(
func_name: &Ident,
block: &Block,
async_context: bool,
async_keyword: bool,
args: &Args,
output_ty: Option<Type>,
) -> Result<TokenStream> {
let name = gen_span_name(func_name, args)?;
let properties = gen_properties(args)?;
let crate_path = &args.crate_path;
let output_ty_hint = if let Some(mut ty) = output_ty {
struct EraseImplTrait;
impl VisitMut for EraseImplTrait {
fn visit_type_mut(&mut self, ty: &mut Type) {
if let Type::ImplTrait(..) = ty {
*ty = Type::Infer(TypeInfer {
underscore_token: Token),
});
} else {
visit_mut::visit_type_mut(self, ty);
}
}
}
EraseImplTrait.visit_type_mut(&mut ty);
ty
} else {
parse_quote!(_)
};
if async_context {
let block = if args.enter_on_poll {
quote!(
#crate_path::future::FutureExt::enter_on_poll(
async move { #block },
#name
)
)
} else {
quote!(
{
let __span__ = #crate_path::Span::enter_with_local_parent( #name ) #properties;
#crate_path::future::FutureExt::in_span(
async move {
let __ret__: #output_ty_hint = #block;
#[allow(unreachable_code)]
__ret__
},
__span__,
)
}
)
};
if async_keyword {
Ok(quote!(
#block.await
))
} else {
Ok(block)
}
} else {
if args.enter_on_poll {
Err(Error::new(
Span::call_site(),
"`enter_on_poll` can not be applied on non-async function",
))
} else {
Ok(quote!(
let __guard__ = #crate_path::local::LocalSpan::enter_with_local_parent( #name ) #properties;
#block
))
}
}
}
enum AsyncTraitKind<'a> {
Function,
Async(&'a ExprAsync),
}
struct AsyncTraitInfo<'a> {
#[expect(unused)]
stmt: &'a Stmt,
kind: AsyncTraitKind<'a>,
}
fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option<AsyncTraitInfo<'_>> {
if block_is_async {
return None;
}
let inside_fns = block.stmts.iter().filter_map(|stmt| {
if let Stmt::Item(Item::Fn(fun)) = &stmt {
if fun.sig.asyncness.is_some() {
return Some((stmt, fun));
}
}
None
});
let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| {
if let Stmt::Expr(expr, None) = stmt {
Some((stmt, expr))
} else {
None
}
})?;
let (outside_func, outside_args) = match last_expr {
Expr::Call(ExprCall { func, args, .. }) => (func, args),
_ => return None,
};
let path = match outside_func.as_ref() {
Expr::Path(path) => &path.path,
_ => return None,
};
if !path_to_string(path).ends_with("Box::pin") {
return None;
}
if outside_args.is_empty() {
return None;
}
if let Expr::Async(async_expr) = &outside_args[0] {
async_expr.capture?;
return Some(AsyncTraitInfo {
stmt: last_expr_stmt,
kind: AsyncTraitKind::Async(async_expr),
});
}
let func = match &outside_args[0] {
Expr::Call(ExprCall { func, .. }) => func,
_ => return None,
};
let func_name = match **func {
Expr::Path(ref func_path) => path_to_string(&func_path.path),
_ => return None,
};
let (stmt_func_declaration, _) = inside_fns
.into_iter()
.find(|(_, fun)| fun.sig.ident == func_name)?;
Some(AsyncTraitInfo {
stmt: stmt_func_declaration,
kind: AsyncTraitKind::Function,
})
}
fn path_to_string(path: &Path) -> String {
use std::fmt::Write;
let mut res = String::with_capacity(path.segments.len() * 5);
for i in 0..path.segments.len() {
write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail");
if i < path.segments.len() - 1 {
res.push_str("::");
}
}
res
}