use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Ident, LitStr, Token,
};
#[derive(Debug, Clone, Default, PartialEq)]
pub enum ConditionalMode {
#[default]
Always,
DebugOnly,
ReleaseOnly,
Feature(String),
}
impl ConditionalMode {
pub fn cfg_tokens(&self) -> Option<proc_macro2::TokenStream> {
use quote::quote;
match self {
ConditionalMode::Always => None,
ConditionalMode::DebugOnly => Some(quote! { #[cfg(debug_assertions)] }),
ConditionalMode::ReleaseOnly => Some(quote! { #[cfg(not(debug_assertions))] }),
ConditionalMode::Feature(name) => {
let feature_name = syn::LitStr::new(name, proc_macro2::Span::call_site());
Some(quote! { #[cfg(feature = #feature_name)] })
}
}
}
pub fn is_conditional(&self) -> bool {
!matches!(self, ConditionalMode::Always)
}
}
#[derive(Debug, Clone, Default)]
pub struct TraceConfig {
pub track_new: bool,
pub track_move: bool,
pub track_drop: bool,
pub track_borrow: bool,
pub track_smart_pointers: bool,
pub track_loops: bool,
pub track_branches: bool,
pub track_control_flow: bool,
pub track_try: bool,
pub track_methods: bool,
pub track_async: bool,
pub track_unsafe: bool,
pub track_expressions: bool,
pub track_functions: bool,
pub conditional_mode: ConditionalMode,
pub warn_ambiguous: bool,
pub known_ffi: Vec<String>,
pub known_unions: Vec<String>,
pub known_statics: Vec<String>,
pub filter_pattern: Option<String>,
pub sample_rate: Option<f64>,
}
impl TraceConfig {
pub fn standard() -> Self {
Self {
track_new: true,
track_move: true,
track_drop: true,
track_borrow: true,
track_smart_pointers: true,
track_loops: true,
track_branches: true,
track_control_flow: true,
track_try: true,
track_methods: true,
track_async: true,
track_unsafe: true,
track_expressions: true,
track_functions: false,
conditional_mode: ConditionalMode::Always,
warn_ambiguous: false,
known_ffi: Vec::new(),
known_unions: Vec::new(),
known_statics: Vec::new(),
filter_pattern: None,
sample_rate: None,
}
}
pub fn quiet() -> Self {
Self {
track_new: true,
track_move: true,
track_drop: true,
track_borrow: true,
track_smart_pointers: false,
track_loops: false,
track_branches: false,
track_control_flow: false,
track_try: false,
track_methods: false,
track_async: false,
track_unsafe: false,
track_expressions: false,
track_functions: false,
conditional_mode: ConditionalMode::Always,
warn_ambiguous: false,
known_ffi: Vec::new(),
known_unions: Vec::new(),
known_statics: Vec::new(),
filter_pattern: None,
sample_rate: None,
}
}
pub fn verbose() -> Self {
Self::standard()
}
pub fn skip(&mut self, features: &str) {
for feature in features.split(',').map(|s| s.trim()) {
match feature {
"loops" => self.track_loops = false,
"branches" => self.track_branches = false,
"control_flow" | "control" => self.track_control_flow = false,
"try" => self.track_try = false,
"methods" => self.track_methods = false,
"async" => self.track_async = false,
"unsafe" => self.track_unsafe = false,
"expressions" | "exprs" => self.track_expressions = false,
"smart_pointers" | "pointers" => self.track_smart_pointers = false,
"functions" | "fn" => self.track_functions = false,
"drop" | "drops" => self.track_drop = false,
_ => {} }
}
}
pub fn only(&mut self, features: &str) {
let mode = self.conditional_mode.clone();
let warn = self.warn_ambiguous;
let ffi = std::mem::take(&mut self.known_ffi);
let unions = std::mem::take(&mut self.known_unions);
let statics = std::mem::take(&mut self.known_statics);
let filter = self.filter_pattern.take();
let sample = self.sample_rate.take();
*self = Self {
track_new: false,
track_move: false,
track_drop: false,
track_borrow: false,
track_smart_pointers: false,
track_loops: false,
track_branches: false,
track_control_flow: false,
track_try: false,
track_methods: false,
track_async: false,
track_unsafe: false,
track_expressions: false,
track_functions: false,
conditional_mode: mode,
warn_ambiguous: warn,
known_ffi: ffi,
known_unions: unions,
known_statics: statics,
filter_pattern: filter,
sample_rate: sample,
};
for feature in features.split(',').map(|s| s.trim()) {
match feature {
"ownership" => {
self.track_new = true;
self.track_move = true;
self.track_drop = true;
self.track_borrow = true;
}
"new" => self.track_new = true,
"move" | "moves" => self.track_move = true,
"drop" | "drops" => self.track_drop = true,
"borrow" | "borrows" => self.track_borrow = true,
"loops" => self.track_loops = true,
"branches" => self.track_branches = true,
"control_flow" | "control" => self.track_control_flow = true,
"try" => self.track_try = true,
"methods" => self.track_methods = true,
"async" => self.track_async = true,
"unsafe" => self.track_unsafe = true,
"expressions" | "exprs" => self.track_expressions = true,
"smart_pointers" | "pointers" => self.track_smart_pointers = true,
"functions" | "fn" => self.track_functions = true,
_ => {} }
}
}
}
pub struct TraceArgs {
pub config: TraceConfig,
}
impl Default for TraceArgs {
fn default() -> Self {
Self {
config: TraceConfig::standard(),
}
}
}
impl Parse for TraceArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self::default());
}
let mut config = TraceConfig::standard();
let args: Punctuated<TraceArg, Token![,]> = Punctuated::parse_terminated(input)?;
for arg in args {
match arg {
TraceArg::Verbose => config = TraceConfig::verbose(),
TraceArg::Quiet => config = TraceConfig::quiet(),
TraceArg::Skip(features) => config.skip(&features),
TraceArg::Only(features) => config.only(&features),
TraceArg::DebugOnly => config.conditional_mode = ConditionalMode::DebugOnly,
TraceArg::ReleaseOnly => config.conditional_mode = ConditionalMode::ReleaseOnly,
TraceArg::Feature(name) => config.conditional_mode = ConditionalMode::Feature(name),
TraceArg::WarnAmbiguous => config.warn_ambiguous = true,
TraceArg::Ffi(names) => config.known_ffi.extend(names),
TraceArg::Unions(names) => config.known_unions.extend(names),
TraceArg::Statics(names) => config.known_statics.extend(names),
TraceArg::Filter(pattern) => config.filter_pattern = Some(pattern),
TraceArg::Sample(rate) => config.sample_rate = Some(rate),
}
}
Ok(Self { config })
}
}
#[derive(Debug)]
enum TraceArg {
Verbose,
Quiet,
Skip(String),
Only(String),
DebugOnly,
ReleaseOnly,
Feature(String),
WarnAmbiguous,
Ffi(Vec<String>),
Unions(Vec<String>),
Statics(Vec<String>),
Filter(String),
Sample(f64),
}
impl Parse for TraceArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident: Ident = input.parse()?;
let name = ident.to_string();
match name.as_str() {
"verbose" => Ok(TraceArg::Verbose),
"quiet" => Ok(TraceArg::Quiet),
"debug_only" => Ok(TraceArg::DebugOnly),
"release_only" => Ok(TraceArg::ReleaseOnly),
"skip" => {
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
Ok(TraceArg::Skip(value.value()))
} else {
let content;
syn::parenthesized!(content in input);
let items: Punctuated<Ident, Token![,]> = Punctuated::parse_terminated(&content)?;
let value = items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ");
Ok(TraceArg::Skip(value))
}
}
"only" => {
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
Ok(TraceArg::Only(value.value()))
} else {
let content;
syn::parenthesized!(content in input);
let items: Punctuated<Ident, Token![,]> = Punctuated::parse_terminated(&content)?;
let value = items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ");
Ok(TraceArg::Only(value))
}
}
"feature" => {
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
Ok(TraceArg::Feature(value.value()))
}
"warn" | "warn_ambiguous" => Ok(TraceArg::WarnAmbiguous),
"ffi" => {
input.parse::<Token![=]>()?;
let names = parse_string_list(input)?;
Ok(TraceArg::Ffi(names))
}
"unions" => {
input.parse::<Token![=]>()?;
let names = parse_string_list(input)?;
Ok(TraceArg::Unions(names))
}
"statics" => {
input.parse::<Token![=]>()?;
let names = parse_string_list(input)?;
Ok(TraceArg::Statics(names))
}
"filter" => {
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
let pattern = value.value();
if pattern.is_empty() {
return Err(syn::Error::new(
value.span(),
"filter pattern cannot be empty"
));
}
for ch in pattern.chars() {
if !ch.is_alphanumeric() && ch != '_' && ch != '*' && ch != '?' {
return Err(syn::Error::new(
value.span(),
format!(
"invalid character '{}' in filter pattern. \
Only alphanumeric, '_', '*', and '?' are allowed",
ch
)
));
}
}
Ok(TraceArg::Filter(pattern))
}
"sample" => {
input.parse::<Token![=]>()?;
let value: syn::LitFloat = input.parse()?;
let rate: f64 = value.base10_parse()?;
if rate < 0.0 || rate > 1.0 {
return Err(syn::Error::new(
value.span(),
format!(
"sample rate must be between 0.0 and 1.0, got {}",
rate
)
));
}
Ok(TraceArg::Sample(rate))
}
_ => Err(syn::Error::new(
ident.span(),
format!(
"unknown attribute argument: {}. Expected one of: verbose, quiet, \
debug_only, release_only, skip, only, feature, warn, ffi, unions, statics, filter, sample",
name
),
)),
}
}
}
fn parse_string_list(input: ParseStream) -> syn::Result<Vec<String>> {
let content;
syn::bracketed!(content in input);
let items: Punctuated<LitStr, Token![,]> = Punctuated::parse_terminated(&content)?;
Ok(items.iter().map(|s| s.value()).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_config() {
let config = TraceConfig::standard();
assert!(config.track_new);
assert!(config.track_loops);
assert!(config.track_branches);
assert_eq!(config.conditional_mode, ConditionalMode::Always);
}
#[test]
fn test_quiet_config() {
let config = TraceConfig::quiet();
assert!(config.track_new);
assert!(!config.track_loops);
assert!(!config.track_branches);
assert_eq!(config.conditional_mode, ConditionalMode::Always);
}
#[test]
fn test_skip() {
let mut config = TraceConfig::standard();
config.skip("loops, branches");
assert!(!config.track_loops);
assert!(!config.track_branches);
assert!(config.track_new);
}
#[test]
fn test_only_ownership() {
let mut config = TraceConfig::standard();
config.only("ownership");
assert!(config.track_new);
assert!(config.track_move);
assert!(config.track_drop);
assert!(config.track_borrow);
assert!(!config.track_loops);
assert!(!config.track_branches);
}
#[test]
fn test_conditional_mode_debug_only() {
let mut config = TraceConfig::standard();
config.conditional_mode = ConditionalMode::DebugOnly;
assert!(config.conditional_mode.is_conditional());
assert!(config.conditional_mode.cfg_tokens().is_some());
}
#[test]
fn test_conditional_mode_release_only() {
let mut config = TraceConfig::standard();
config.conditional_mode = ConditionalMode::ReleaseOnly;
assert!(config.conditional_mode.is_conditional());
assert!(config.conditional_mode.cfg_tokens().is_some());
}
#[test]
fn test_conditional_mode_feature() {
let mut config = TraceConfig::standard();
config.conditional_mode = ConditionalMode::Feature("tracing".to_string());
assert!(config.conditional_mode.is_conditional());
assert!(config.conditional_mode.cfg_tokens().is_some());
}
#[test]
fn test_conditional_mode_always() {
let config = TraceConfig::standard();
assert!(!config.conditional_mode.is_conditional());
assert!(config.conditional_mode.cfg_tokens().is_none());
}
#[test]
fn test_only_preserves_conditional_mode() {
let mut config = TraceConfig::standard();
config.conditional_mode = ConditionalMode::DebugOnly;
config.only("ownership");
assert_eq!(config.conditional_mode, ConditionalMode::DebugOnly);
}
#[test]
fn test_filter_pattern() {
let mut config = TraceConfig::standard();
config.filter_pattern = Some("data*".to_string());
assert_eq!(config.filter_pattern, Some("data*".to_string()));
}
#[test]
fn test_sample_rate() {
let mut config = TraceConfig::standard();
config.sample_rate = Some(0.1);
assert_eq!(config.sample_rate, Some(0.1));
}
#[test]
fn test_only_preserves_filter_and_sample() {
let mut config = TraceConfig::standard();
config.filter_pattern = Some("user*".to_string());
config.sample_rate = Some(0.5);
config.only("ownership");
assert_eq!(config.filter_pattern, Some("user*".to_string()));
assert_eq!(config.sample_rate, Some(0.5));
}
#[test]
fn test_filter_pattern_validation_valid() {
let valid = ["data*", "*_count", "user_?", "abc123", "a*b?c"];
for pattern in valid {
let input = format!("filter = \"{}\"", pattern);
let result: syn::Result<TraceArg> = syn::parse_str(&input);
assert!(result.is_ok(), "Pattern '{}' should be valid", pattern);
}
}
#[test]
fn test_filter_pattern_validation_empty() {
let result: syn::Result<TraceArg> = syn::parse_str("filter = \"\"");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_filter_pattern_validation_invalid_chars() {
let invalid = ["data-name", "user.name", "path/to", "a@b", "x#y"];
for pattern in invalid {
let input = format!("filter = \"{}\"", pattern);
let result: syn::Result<TraceArg> = syn::parse_str(&input);
assert!(result.is_err(), "Pattern '{}' should be invalid", pattern);
assert!(result.unwrap_err().to_string().contains("invalid character"));
}
}
#[test]
fn test_sample_rate_validation_valid() {
let valid = ["0.0", "0.1", "0.5", "1.0"];
for rate in valid {
let input = format!("sample = {}", rate);
let result: syn::Result<TraceArg> = syn::parse_str(&input);
assert!(result.is_ok(), "Rate {} should be valid", rate);
}
}
#[test]
fn test_sample_rate_validation_out_of_range() {
let invalid = ["-0.1", "1.1", "2.0", "-1.0"];
for rate in invalid {
let input = format!("sample = {}", rate);
let result: syn::Result<TraceArg> = syn::parse_str(&input);
assert!(result.is_err(), "Rate {} should be invalid", rate);
assert!(result.unwrap_err().to_string().contains("between 0.0 and 1.0"));
}
}
}