#![doc(html_root_url = "https://docs.rs/tracing-attributes/0.1.5")]
#![deny(missing_debug_implementations, unreachable_pub)]
#![cfg_attr(test, deny(warnings))]
extern crate proc_macro;
use std::collections::HashSet;
use std::iter;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
spanned::Spanned, AttributeArgs, FieldPat, FnArg, Ident, ItemFn, Lit, LitInt, Meta, MetaList,
MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct,
PatType, Signature,
};
#[proc_macro_attribute]
pub fn instrument(args: TokenStream, item: TokenStream) -> TokenStream {
let input: ItemFn = syn::parse_macro_input!(item as ItemFn);
let args = syn::parse_macro_input!(args as AttributeArgs);
let ItemFn {
attrs,
vis,
block,
sig,
..
} = input;
let Signature {
output: return_type,
inputs: params,
unsafety,
asyncness,
constness,
abi,
ident,
generics:
syn::Generics {
params: gen_params,
where_clause,
..
},
..
} = sig;
let ident_str = ident.to_string();
let skips = match skips(&args) {
Ok(skips) => skips,
Err(err) => return quote!(#err).into(),
};
let param_names: Vec<Ident> = params
.clone()
.into_iter()
.flat_map(|param| match param {
FnArg::Typed(PatType { pat, .. }) => param_names(*pat),
FnArg::Receiver(_) => Box::new(iter::once(Ident::new("self", param.span()))),
})
.filter(|ident| !skips.contains(ident))
.collect();
let param_names_clone = param_names.clone();
let body = if asyncness.is_some() {
let async_kwd = syn::token::Async { span: block.span() };
let await_kwd = syn::Ident::new("await", block.span());
quote_spanned! {block.span()=>
tracing_futures::Instrument::instrument(
#async_kwd move { #block },
__tracing_attr_span
)
.#await_kwd
}
} else {
quote_spanned!(block.span()=>
let __tracing_attr_guard = __tracing_attr_span.enter();
#block
)
};
let level = level(&args);
let target = target(&args);
let span_name = name(&args, ident_str);
quote!(
#(#attrs) *
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
let __tracing_attr_span = tracing::span!(
target: #target,
#level,
#span_name,
#(#param_names = tracing::field::debug(&#param_names_clone)),*
);
#body
}
)
.into()
}
fn param_names(pat: Pat) -> Box<dyn Iterator<Item = Ident>> {
match pat {
Pat::Ident(PatIdent { ident, .. }) => Box::new(iter::once(ident)),
Pat::Reference(PatReference { pat, .. }) => param_names(*pat),
Pat::Struct(PatStruct { fields, .. }) => Box::new(
fields
.into_iter()
.flat_map(|FieldPat { pat, .. }| param_names(*pat)),
),
Pat::Tuple(PatTuple { elems, .. }) => Box::new(elems.into_iter().flat_map(param_names)),
Pat::TupleStruct(PatTupleStruct {
pat: PatTuple { elems, .. },
..
}) => Box::new(elems.into_iter().flat_map(param_names)),
_ => Box::new(iter::empty()),
}
}
fn skips(args: &AttributeArgs) -> Result<HashSet<Ident>, impl ToTokens> {
let mut skips = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::List(MetaList {
ref path,
ref nested,
..
})) if path.is_ident("skip") => Some(nested),
_ => None,
});
let skip = skips.next();
if let Some(list) = skips.next() {
return Err(quote_spanned! {
list.span() => compile_error!("expected only a single `skip` argument!")
});
}
Ok(skip
.iter()
.map(|list| list.iter())
.flatten()
.filter_map(|meta| match meta {
NestedMeta::Meta(Meta::Path(p)) => p.get_ident().map(Clone::clone),
_ => None,
})
.collect())
}
fn level(args: &AttributeArgs) -> impl ToTokens {
let mut levels = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("level") => Some(lit.clone()),
_ => None,
});
let level = levels.next();
if let Some(lit) = levels.next() {
return quote_spanned! {lit.span()=>
compile_error!("expected only a single `level` argument!")
};
}
fn is_level(lit: &LitInt, expected: u64) -> bool {
match lit.base10_parse::<u64>() {
Ok(value) => value == expected,
Err(_) => false,
}
}
match level {
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("trace") => {
quote!(tracing::Level::TRACE)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("debug") => {
quote!(tracing::Level::DEBUG)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("info") => {
quote!(tracing::Level::INFO)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("warn") => {
quote!(tracing::Level::WARN)
}
Some(Lit::Str(ref lit)) if lit.value().eq_ignore_ascii_case("error") => {
quote!(tracing::Level::ERROR)
}
Some(Lit::Int(ref lit)) if is_level(lit, 1) => quote!(tracing::Level::TRACE),
Some(Lit::Int(ref lit)) if is_level(lit, 2) => quote!(tracing::Level::DEBUG),
Some(Lit::Int(ref lit)) if is_level(lit, 3) => quote!(tracing::Level::INFO),
Some(Lit::Int(ref lit)) if is_level(lit, 4) => quote!(tracing::Level::WARN),
Some(Lit::Int(ref lit)) if is_level(lit, 5) => quote!(tracing::Level::ERROR),
Some(lit) => quote_spanned! {lit.span()=>
compile_error!(
"unknown verbosity level, expected one of \"trace\", \
\"debug\", \"info\", \"warn\", or \"error\", or a number 1-5"
)
},
None => quote!(tracing::Level::INFO),
}
}
fn target(args: &AttributeArgs) -> impl ToTokens {
let mut levels = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("target") => Some(lit.clone()),
_ => None,
});
let level = levels.next();
if let Some(lit) = levels.next() {
return quote_spanned! {lit.span()=>
compile_error!("expected only a single `target` argument!")
};
}
match level {
Some(Lit::Str(ref lit)) => quote!(#lit),
Some(lit) => quote_spanned! {lit.span()=>
compile_error!(
"expected target to be a string literal"
)
},
None => quote!(module_path!()),
}
}
fn name(args: &AttributeArgs, default_name: String) -> impl ToTokens {
let mut names = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
ref path, ref lit, ..
})) if path.is_ident("name") => Some(lit.clone()),
_ => None,
});
let name = names.next();
if let Some(lit) = names.next() {
return quote_spanned! {lit.span() =>
compile_error!("expected only a single `name` argument!")
};
}
match name {
Some(Lit::Str(ref lit)) => quote!(#lit),
Some(lit) => {
quote_spanned! { lit.span() => compile_error!("expected name to be a string literal") }
}
None => quote!(#default_name),
}
}