#![doc(html_root_url = "https://docs.rs/tracing-attributes/0.1.8")]
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
#![allow(unused)]
extern crate proc_macro;
use std::collections::{HashMap, HashSet};
use std::iter;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
spanned::Spanned, AttributeArgs, Block, Expr, ExprCall, FieldPat, FnArg, Ident, Item, ItemFn,
Lit, LitInt, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct,
PatTuple, PatTupleStruct, PatType, Path, Signature, Stmt,
};
#[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);
if let Some(internal_fun_name) =
get_async_trait_name(&input.block, input.sig.asyncness.is_some())
{
let mut stmts: Vec<Stmt> = input.block.stmts.to_vec();
for stmt in &mut stmts {
if let Stmt::Item(Item::Fn(fun)) = stmt {
if fun.sig.ident == internal_fun_name {
*stmt = syn::parse2(gen_body(fun, args, Some(input.sig.ident.to_string())))
.unwrap();
break;
}
}
}
let sig = &input.sig;
let attrs = &input.attrs;
quote!(
#(#attrs) *
#sig {
#(#stmts) *
}
)
.into()
} else {
gen_body(&input, args, None).into()
}
}
fn gen_body(
input: &ItemFn,
args: AttributeArgs,
fun_name: Option<String>,
) -> proc_macro2::TokenStream {
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 = if let Some(x) = &fun_name {
x.clone()
} else {
ident.to_string()
};
let span = (|| {
let skips = match skips(&args) {
Ok(skips) => skips,
Err(err) => return quote!(#err),
};
let param_names: Vec<(Ident, 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()))),
})
.map(|x| {
if fun_name.is_some() && x == "_self" {
(Ident::new("self", x.span()), x)
} else {
(x.clone(), x)
}
})
.collect();
for skip in &skips {
if !param_names.iter().map(|(user, _)| user).any(|y| y == skip) {
return quote_spanned! {skip.span()=>
compile_error!("attempting to skip non-existent parameter")
};
}
}
let param_names: Vec<(Ident, Ident)> = param_names
.into_iter()
.filter(|(ident, _)| !skips.contains(ident))
.collect();
let new_param_names: Vec<&Ident> = param_names.iter().map(|x| &x.0).collect();
let fields = match fields(&args, &new_param_names) {
Ok(fields) => fields,
Err(err) => return quote!(#err),
};
let level = level(&args);
let target = target(&args);
let span_name = name(&args, ident_str);
let mut quoted_fields: Vec<_> = param_names
.iter()
.map(|(user_name, real_name)| quote!(#user_name = tracing::field::debug(&#real_name)))
.collect();
quoted_fields.extend(fields.into_iter().map(|(key, value)| {
let value = match value {
Some(value) => quote!(#value),
None => quote!(tracing::field::Empty),
};
quote!(#key = #value)
}));
quote!(tracing::span!(
target: #target,
#level,
#span_name,
#(#quoted_fields),*
))
})();
let body = if asyncness.is_some() {
if instrument_err(&args) {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
tracing_futures::Instrument::instrument(async move {
match async move { #block }.await {
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
Err(e)
}
}
}, __tracing_attr_span).await
)
} else {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
tracing_futures::Instrument::instrument(
async move { #block },
__tracing_attr_span
)
.await
)
}
} else if instrument_err(&args) {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
match { #block } {
Ok(x) => Ok(x),
Err(e) => {
tracing::error!(error = %e);
Err(e)
}
}
)
} else {
quote_spanned!(block.span()=>
let __tracing_attr_span = #span;
let __tracing_attr_guard = __tracing_attr_span.enter();
#block
)
};
quote!(
#(#attrs) *
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
#where_clause
{
#body
}
)
}
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: &[NestedMeta]) -> 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: &[NestedMeta]) -> 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: &[NestedMeta]) -> impl ToTokens {
let mut targets = 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 target = targets.next();
if let Some(lit) = targets.next() {
return quote_spanned! {lit.span()=>
compile_error!("expected only a single `target` argument!")
};
}
match target {
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 fields(
args: &[NestedMeta],
param_names: &[&Ident],
) -> Result<(Vec<(Ident, Option<Lit>)>), impl ToTokens> {
let mut fields = args.iter().filter_map(|arg| match arg {
NestedMeta::Meta(Meta::List(MetaList {
ref path,
ref nested,
..
})) if path.is_ident("fields") => Some(nested.clone()),
_ => None,
});
let field_holder = fields.next();
if let Some(lit) = fields.next() {
return Err(quote_spanned! {lit.span()=>
compile_error!("expected only a single `fields` argument!")
});
}
match field_holder {
Some(fields) => {
let mut parsed = Vec::default();
let mut visited_keys: HashSet<String> = Default::default();
let param_set: HashSet<String> = param_names.iter().map(|i| i.to_string()).collect();
for field in fields.into_iter() {
let (key, value) = match field {
NestedMeta::Meta(meta) => match meta {
Meta::NameValue(kv) => (kv.path, Some(kv.lit)),
Meta::Path(path) => (path, None),
_ => {
return Err(quote_spanned! {meta.span()=>
compile_error!("each field must be a key with an optional value. Keys must be valid Rust identifiers (nested keys with dots are not supported).")
})
}
},
_ => {
return Err(quote_spanned! {field.span()=>
compile_error!("`fields` argument should be a list of key-value fields")
})
}
};
let key = match key.get_ident() {
Some(key) => key,
None => {
return Err(quote_spanned! {key.span()=>
compile_error!("field keys must be valid Rust identifiers (nested keys with dots are not supported).")
})
}
};
let key_str = key.to_string();
if param_set.contains(&key_str) {
return Err(quote_spanned! {key.span()=>
compile_error!("field overlaps with (non-skipped) parameter name")
});
}
if visited_keys.contains(&key_str) {
return Err(quote_spanned! {key.span()=>
compile_error!("each field key must appear at most once")
});
} else {
visited_keys.insert(key_str);
}
if let Some(literal) = &value {
match literal {
Lit::Bool(_) | Lit::Str(_) | Lit::Int(_) => {}
_ => {
return Err(quote_spanned! {literal.span()=>
compile_error!("values can be only strings, integers or booleans")
})
}
}
}
parsed.push((key.clone(), value));
}
Ok(parsed)
}
None => Ok(Default::default()),
}
}
fn name(args: &[NestedMeta], 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),
}
}
fn instrument_err(args: &[NestedMeta]) -> bool {
args.iter().any(|arg| match arg {
NestedMeta::Meta(Meta::Path(path)) => path.is_ident("err"),
_ => false,
})
}
fn get_async_trait_name(block: &Block, block_is_async: bool) -> Option<String> {
if block_is_async {
return None;
}
let mut inside_funs = Vec::new();
let mut last_expr = None;
for stmt in &block.stmts {
if let Stmt::Item(Item::Fn(fun)) = &stmt {
if fun.sig.asyncness.is_some() {
inside_funs.push(fun.sig.ident.to_string());
}
} else if let Stmt::Expr(e) = &stmt {
last_expr = Some(e);
}
}
if let Some(Expr::Call(ExprCall {
func: outside_func,
args: outside_args,
..
})) = last_expr
{
if let Expr::Path(path) = outside_func.as_ref() {
if "Box::pin" == path_to_string(&path.path) {
if outside_args.is_empty() {
return None;
}
if let Expr::Call(ExprCall { func, args, .. }) = &outside_args[0] {
if let Expr::Path(inside_path) = func.as_ref() {
let func_name = path_to_string(&inside_path.path);
if inside_funs.contains(&func_name) {
return Some(func_name);
}
}
}
}
}
}
None
}
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!(&mut res, "{}", path.segments[i].ident)
.expect("writing to a String should never fail");
if i < path.segments.len() - 1 {
res.push_str("::");
}
}
res
}