use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, Parser};
use syn::punctuated::Punctuated;
#[allow(unused_imports)]
use syn::{
parenthesized, parse_quote,
visit_mut::{self, VisitMut},
Expr, FnArg, Ident, MetaNameValue, Pat, Token,
};
const WITH_CONTEXT_ENABLED: bool = cfg!(feature = "with_context");
struct BlockRewriter;
impl VisitMut for BlockRewriter {
fn visit_macro_mut(&mut self, mac: &mut syn::Macro) {
let path = &mac.path;
if let Some(last_segment) = path.segments.last() {
if last_segment.ident == "info"
|| last_segment.ident == "warn"
|| last_segment.ident == "error"
|| last_segment.ident == "debug"
|| last_segment.ident == "trace"
{
if let Some(first_segment) = path.segments.first() {
if first_segment.ident == "tracing" {
let mut new_path = path.clone();
new_path.segments = new_path.segments.into_iter().skip(1).collect();
mac.path = new_path;
}
}
}
}
visit_mut::visit_macro_mut(self, mac);
}
}
struct SpawnInstrumentRewriter;
impl VisitMut for SpawnInstrumentRewriter {
fn visit_expr_mut(&mut self, expr: &mut syn::Expr) {
if let syn::Expr::Call(expr_call) = expr {
if let syn::Expr::Path(expr_path) = &*expr_call.func {
if expr_path.path.segments.iter().any(|s| s.ident == "spawn") {
if let Some(fut_arg) = expr_call.args.first_mut() {
let original_fut = fut_arg.clone();
*fut_arg = parse_quote! {
::log_args_runtime::instrument_spawn(#original_fut)
};
}
}
}
}
visit_mut::visit_expr_mut(self, expr);
}
}
#[cfg(feature = "function-names-camel")]
#[allow(dead_code)]
fn to_camel_case(snake_case: &str) -> String {
let mut camel_case = String::new();
let mut capitalize = false;
for c in snake_case.chars() {
if c == '_' {
capitalize = true;
} else if capitalize {
camel_case.push(c.to_ascii_uppercase());
capitalize = false;
} else {
camel_case.push(c);
}
}
camel_case
}
#[cfg(feature = "function-names-screaming")]
#[allow(dead_code)]
fn to_screaming_snake_case(snake_case: &str) -> String {
snake_case.to_ascii_uppercase()
}
#[cfg(feature = "function-names-kebab")]
#[allow(dead_code)]
fn to_kebab_case(snake_case: &str) -> String {
snake_case.replace('_', "-")
}
#[cfg(any(feature = "function-names-pascal", feature = "function-names"))]
fn to_pascal_case(snake_case: &str) -> String {
let mut pascal_case = String::new();
let mut capitalize = true; for c in snake_case.chars() {
if c == '_' {
capitalize = true;
} else if capitalize {
pascal_case.push(c.to_ascii_uppercase());
capitalize = false;
} else {
pascal_case.push(c);
}
}
pascal_case
}
#[allow(dead_code)]
fn get_formatted_function_name(function_name: &str) -> String {
#[cfg(feature = "function-names-camel")]
{
return to_camel_case(function_name);
}
#[cfg(any(feature = "function-names-pascal", feature = "function-names"))]
{
return to_pascal_case(function_name);
}
#[cfg(feature = "function-names-screaming")]
{
return to_screaming_snake_case(function_name);
}
#[cfg(feature = "function-names-kebab")]
{
return to_kebab_case(function_name);
}
#[cfg(feature = "function-names-snake")]
{
return function_name.to_string();
}
#[allow(unreachable_code)]
function_name.to_string()
}
#[proc_macro_attribute]
pub fn params(args: TokenStream, input: TokenStream) -> TokenStream {
let mut item = if let Ok(item_fn) = syn::parse::<syn::ItemFn>(input.clone()) {
FnItem::Item(item_fn)
} else if let Ok(impl_item_fn) = syn::parse::<syn::ImplItemFn>(input.clone()) {
FnItem::ImplItem(impl_item_fn)
} else {
return syn::Error::new_spanned(
proc_macro2::TokenStream::from(input),
"The #[params] attribute can only be applied to functions or methods.",
)
.to_compile_error()
.into();
};
let allow_unused_macros_attr: syn::Attribute = syn::parse_quote! { #[allow(unused_macros)] };
item.attrs_mut().push(allow_unused_macros_attr);
let attrs = match Punctuated::<Attribute, Token![,]>::parse_terminated.parse(args) {
Ok(attrs) => attrs,
Err(e) => return e.to_compile_error().into(),
};
let config = AttrConfig::from_attributes(attrs);
let (context_fields, clone_stmts) = get_context_fields_quote(&item, &config);
let is_async = item.sig().asyncness.is_some();
let new_block_tokens = generate_new_block(&item, &config, &context_fields, is_async, clone_stmts);
*item.block_mut() = match syn::parse2(new_block_tokens) {
Ok(block) => block,
Err(e) => return e.to_compile_error().into(),
};
TokenStream::from(quote! { #item })
}
fn generate_new_block(
item: &FnItem,
config: &AttrConfig,
context_fields: &[proc_macro2::TokenStream],
is_async: bool,
clone_stmts: Vec<proc_macro2::TokenStream>,
) -> proc_macro2::TokenStream {
let log_redefines = get_log_redefines_with_fields(context_fields, is_async);
let original_block = item.block().clone();
let mut transformed_block = original_block.clone();
BlockRewriter.visit_block_mut(&mut transformed_block);
SpawnInstrumentRewriter.visit_block_mut(&mut transformed_block);
if config.span {
let context_map = get_context_map_for_span(item, config);
let auto_capture_stmt = if config.auto_capture {
quote! { let _auto_capture_guard = ::log_args_runtime::capture_context(); }
} else {
quote! {}
};
if is_async {
quote! {
{
#(#clone_stmts)*
::log_args_runtime::with_async_context(#context_map, async move {
#auto_capture_stmt
#log_redefines
#transformed_block
}).await
}
}
} else {
quote! {
{
#(#clone_stmts)*
let _context_guard = ::log_args_runtime::push_context(#context_map);
#auto_capture_stmt
#log_redefines
#transformed_block
}
}
}
} else {
quote! {
{
#(#clone_stmts)*
#log_redefines
#transformed_block
}
}
}
}
#[derive(Clone)]
enum FieldKey {
Ident(Ident),
LitStr(syn::LitStr),
}
impl quote::ToTokens for FieldKey {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
FieldKey::Ident(ident) => ident.to_tokens(tokens),
FieldKey::LitStr(lit) => lit.to_tokens(tokens),
}
}
}
impl FieldKey {
fn to_string(&self) -> String {
match self {
FieldKey::Ident(ident) => ident.to_string(),
FieldKey::LitStr(lit) => lit.value(),
}
}
}
#[allow(dead_code)]
#[derive(Clone)]
struct NameValueField {
key: FieldKey,
eq_token: Token![=],
value: Expr,
}
impl Parse for NameValueField {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key = if input.peek(syn::LitStr) {
FieldKey::LitStr(input.parse()?)
} else {
FieldKey::Ident(input.parse()?)
};
let eq_token: Token![=] = input.parse()?;
let value: Expr = input.parse()?;
Ok(NameValueField {
key,
eq_token,
value,
})
}
}
#[derive(Clone)]
enum Field {
NameValue(NameValueField),
Expr(Expr),
}
impl Parse for Field {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if (input.peek(Ident) || input.peek(syn::LitStr)) && input.peek2(Token![=]) {
Ok(Field::NameValue(input.parse()?))
} else {
Ok(Field::Expr(input.parse()?))
}
}
}
enum Attribute {
Fields(Punctuated<Field, Token![,]>),
Custom(Punctuated<NameValueField, Token![,]>),
Current(Punctuated<Expr, Token![,]>),
CloneUpfront,
Span(Punctuated<Expr, Token![,]>),
All,
AutoCapture,
WithContext,
}
impl Parse for Attribute {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: Ident = input.parse()?;
if ident == "fields" {
let content;
parenthesized!(content in input);
let fields = Punctuated::<Field, Token![,]>::parse_terminated(&content)?;
Ok(Attribute::Fields(fields))
} else if ident == "custom" {
let content;
parenthesized!(content in input);
let custom = Punctuated::<NameValueField, Token![,]>::parse_terminated(&content)?;
Ok(Attribute::Custom(custom))
} else if ident == "current" {
let content;
parenthesized!(content in input);
let current = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
Ok(Attribute::Current(current))
} else if ident == "clone_upfront" {
Ok(Attribute::CloneUpfront)
} else if ident == "span" {
if input.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let span_fields = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
Ok(Attribute::Span(span_fields))
} else {
Ok(Attribute::Span(Punctuated::new()))
}
} else if ident == "all" {
Ok(Attribute::All)
} else if ident == "auto_capture" {
Ok(Attribute::AutoCapture)
} else if ident == "with_context" {
Ok(Attribute::WithContext)
} else {
Err(syn::Error::new_spanned(ident, "unknown attribute"))
}
}
}
struct AttrConfig {
fields: Vec<Field>,
custom: Vec<NameValueField>,
current: Vec<syn::Expr>,
clone_upfront: bool,
span: bool,
span_fields: Vec<syn::Expr>,
all_params: bool,
auto_capture: bool,
with_context: bool,
}
impl Default for AttrConfig {
fn default() -> Self {
Self {
fields: Vec::new(),
custom: Vec::new(),
current: Vec::new(),
clone_upfront: true, span: true, span_fields: Vec::new(),
all_params: false,
auto_capture: false, with_context: false,
}
}
}
impl AttrConfig {
fn from_attributes(attrs: Punctuated<Attribute, Token![,]>) -> Self {
let mut config = AttrConfig::default();
for attr in attrs {
match attr {
Attribute::Fields(fields) => config.fields.extend(fields),
Attribute::Custom(custom) => config.custom.extend(custom),
Attribute::Current(current) => config.current.extend(current),
Attribute::CloneUpfront => config.clone_upfront = true,
Attribute::Span(span_fields) => {
config.span = true;
config.clone_upfront = true; config.span_fields.extend(span_fields);
}
Attribute::All => {
config.all_params = true;
}
Attribute::AutoCapture => {
config.auto_capture = true;
config.span = true;
}
Attribute::WithContext => {
config.with_context = true;
config.span = true;
}
}
}
config
}
}
fn get_context_fields_quote(
item: &FnItem,
config: &AttrConfig,
) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
let mut field_assignments = vec![];
let mut clone_statements = vec![];
let mut cloned_fields = std::collections::HashSet::new();
let mut process_expr = |expr: &syn::Expr| -> syn::Expr {
let expr_str = quote!(#expr).to_string();
if config.clone_upfront && expr_str.contains("self.") {
let mut modified_expr_str = expr_str.clone();
let mut start = 0;
while let Some(pos) = modified_expr_str[start..].find("self.") {
let field_start = start + pos + 5; let remaining = &modified_expr_str[field_start..];
let field_end = remaining
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(remaining.len());
let field_name_part = &remaining[..field_end];
let replacement = format!("__{field_name_part}_for_macro");
if cloned_fields.insert(field_name_part.to_string()) {
let field_ident = Ident::new(field_name_part, proc_macro2::Span::call_site());
let replacement_ident = Ident::new(&replacement, proc_macro2::Span::call_site());
clone_statements.push(quote! {
let #replacement_ident = self.#field_ident.clone();
});
}
let old_expr = format!("self.{field_name_part}");
modified_expr_str = modified_expr_str.replace(&old_expr, &replacement);
start = field_start + field_end;
}
syn::parse_str(&modified_expr_str).unwrap_or_else(|_| expr.clone())
} else {
expr.clone()
}
};
if config.span
&& config.fields.is_empty()
&& config.custom.is_empty()
&& config.current.is_empty()
&& !config.all_params
{
if WITH_CONTEXT_ENABLED {
field_assignments.push(quote! {
context = ::log_args_runtime::get_inherited_context_string()
});
}
}
if config.all_params {
let all_args = get_all_args(item);
for ident in all_args {
let ident_str = ident.to_string();
if config.span {
field_assignments.push(quote! {
#ident = ::log_args_runtime::get_context_value_merged(&#ident_str).unwrap_or_else(|| "".to_string())
});
} else {
field_assignments.push(quote! {#ident = ?#ident });
}
}
}
if !config.fields.is_empty() {
for field in &config.fields {
match field {
Field::Expr(expr) => {
let field_name = quote! { #expr }.to_string().replace(' ', "");
if config.span {
field_assignments.push(quote! {
#field_name = ::log_args_runtime::get_context_value_merged(&#field_name).unwrap_or_else(|| "".to_string())
});
} else {
let processed = process_expr(expr);
field_assignments.push(quote! { #field_name = ?#processed });
}
}
Field::NameValue(nv) => {
let key = &nv.key;
let value = &nv.value;
let key_str = key.to_string();
if config.span {
field_assignments.push(quote! {
#key_str = ::log_args_runtime::get_context_value_merged(&#key_str).unwrap_or_else(|| "".to_string())
});
} else {
let processed = process_expr(value);
field_assignments.push(quote! { #key = ?#processed });
}
}
}
}
}
if !config.span_fields.is_empty() {
for field_expr in &config.span_fields {
let field_name = quote! { #field_expr }.to_string().replace(' ', "");
field_assignments.push(quote! {
#field_name = ::log_args_runtime::get_context_value_merged(&#field_name).unwrap_or_else(|| "".to_string())
});
}
}
for nv in &config.custom {
let key = &nv.key;
let value = &nv.value;
let processed = process_expr(value);
field_assignments.push(quote! {
#key = ?#processed
});
}
for current_field in &config.current {
let field_name = quote! { #current_field }.to_string().replace(' ', "");
if config.span {
field_assignments.push(quote! {
#field_name = ::log_args_runtime::get_context_value_merged(&#field_name).unwrap_or_else(|| "".to_string())
});
} else {
let processed = process_expr(current_field);
field_assignments.push(quote! { #field_name = ?#processed });
}
}
add_function_name_field(&mut field_assignments, item);
(field_assignments, clone_statements)
}
#[allow(dead_code, unused_variables)]
fn add_function_name_field(field_assignments: &mut Vec<proc_macro2::TokenStream>, item: &FnItem) {
#[cfg(any(
feature = "function-names-snake",
feature = "function-names-camel",
feature = "function-names-pascal",
feature = "function-names-screaming",
feature = "function-names-kebab",
feature = "function-names"
))]
{
let function_name = item.sig().ident.to_string();
let formatted_name = get_formatted_function_name(&function_name);
field_assignments.push(quote! {
function_name = #formatted_name
});
}
}
fn get_context_map_for_span(item: &FnItem, config: &AttrConfig) -> proc_macro2::TokenStream {
let mut fields_to_log = vec![];
let all_args = get_all_args(item);
let arg_idents: std::collections::HashSet<String> =
all_args.iter().map(|i| i.to_string()).collect();
if config.all_params {
for ident in all_args {
let ident_str = ident.to_string();
fields_to_log.push(quote! {
new_context.insert(#ident_str.to_string(), format!("{:?}", #ident));
});
}
}
if !config.fields.is_empty() {
for field in &config.fields {
match field {
Field::Expr(expr) => {
let key_str = quote!(#expr).to_string().replace(' ', "");
if arg_idents.contains(&key_str) || key_str.starts_with("self.") {
fields_to_log.push(quote! {
new_context.insert(#key_str.to_string(), format!("{:?}", &#expr));
});
}
}
Field::NameValue(nv) => {
let key_str = nv.key.to_string();
let value = &nv.value;
let val_str = quote!(#value).to_string().replace(' ', "");
if arg_idents.contains(&val_str) || val_str.starts_with("self.") {
fields_to_log.push(quote! {
new_context.insert(#key_str.to_string(), format!("{:?}", &#value));
});
}
}
}
}
}
for nv in &config.custom {
let key_str = nv.key.to_string();
let value = &nv.value;
fields_to_log.push(quote! {
new_context.insert(#key_str.to_string(), format!("{:?}", #value));
});
}
quote! {
{
let mut new_context = ::std::collections::HashMap::new();
#(#fields_to_log)*
new_context
}
}
}
fn get_all_args(item: &FnItem) -> Vec<Ident> {
item.sig()
.inputs
.iter()
.filter_map(|arg| {
if let FnArg::Typed(pt) = arg {
if let Pat::Ident(pi) = &*pt.pat {
if pi.ident != "self" {
return Some(pi.ident.clone());
}
}
}
None
})
.collect()
}
enum FnItem {
Item(syn::ItemFn),
ImplItem(syn::ImplItemFn),
}
impl quote::ToTokens for FnItem {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
FnItem::Item(i) => i.to_tokens(tokens),
FnItem::ImplItem(i) => i.to_tokens(tokens),
}
}
}
impl FnItem {
fn attrs_mut(&mut self) -> &mut Vec<syn::Attribute> {
match self {
FnItem::Item(item_fn) => &mut item_fn.attrs,
FnItem::ImplItem(impl_item_fn) => &mut impl_item_fn.attrs,
}
}
fn sig(&self) -> &syn::Signature {
match self {
FnItem::Item(i) => &i.sig,
FnItem::ImplItem(i) => &i.sig,
}
}
fn block(&self) -> &syn::Block {
match self {
FnItem::Item(i) => &i.block,
FnItem::ImplItem(i) => &i.block,
}
}
fn block_mut(&mut self) -> &mut syn::Block {
match self {
FnItem::Item(i) => &mut i.block,
FnItem::ImplItem(i) => &mut i.block,
}
}
}
fn get_log_redefines_with_fields(
context_fields: &[proc_macro2::TokenStream],
_is_async: bool,
) -> proc_macro2::TokenStream {
quote! {
macro_rules! info {
($($t:tt)*) => {
::log_args_runtime::log_with_context!(::tracing::info, ::log_args_runtime::get_context_merged(), #(#context_fields,)* $($t)*)
};
}
macro_rules! warn {
($($t:tt)*) => {
::log_args_runtime::log_with_context!(::tracing::warn, ::log_args_runtime::get_context_merged(), #(#context_fields,)* $($t)*)
};
}
macro_rules! error {
($($t:tt)*) => {
::log_args_runtime::log_with_context!(::tracing::error, ::log_args_runtime::get_context_merged(), #(#context_fields,)* $($t)*)
};
}
macro_rules! debug {
($($t:tt)*) => {
::log_args_runtime::log_with_context!(::tracing::debug, ::log_args_runtime::get_context_merged(), #(#context_fields,)* $($t)*)
};
}
macro_rules! trace {
($($t:tt)*) => {
::log_args_runtime::log_with_context!(::tracing::trace, ::log_args_runtime::get_context_merged(), #(#context_fields,)* $($t)*)
};
}
}
}