#![doc(html_root_url = "https://docs.rs/tracing-attributes/0.1.7")]
#![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, FieldPat, FnArg, Ident, ItemFn, Lit, LitInt, Meta, MetaList,
MetaNameValue, NestedMeta, Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct,
PatType, Path, 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()))),
})
.collect();
for skip in &skips {
if !param_names.contains(skip) {
return quote_spanned!(skip.span()=>
compile_error!("attempting to skip non-existent parameter")
)
.into();
}
}
let param_names: Vec<Ident> = param_names
.into_iter()
.filter(|ident| !skips.contains(ident))
.collect();
let fields = match fields(&args, ¶m_names) {
Ok(fields) => fields,
Err(err) => return quote!(#err).into(),
};
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);
let mut quoted_fields: Vec<_> = param_names
.into_iter()
.map(|i| quote!(#i = tracing::field::debug(&#i)))
.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!(
#(#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,
#(#quoted_fields),*
);
#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: &[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),
}
}