funlog/
lib.rs

1//! # Funlog - Function Logging Macro
2//!
3//! Funlog is a procedural macro that automatically adds logging to functions.
4//! It supports various logging levels, parameter logging, return value logging,
5//! and flexible positioning of log statements.
6//!
7//! ## Features
8//!
9//! - Multiple log levels: `print`, `trace`, `debug`, `info`, `warn`, `error`
10//! - Parameter control: `all`, `none`, or `params(param1, param2)`
11//! - Position control: `onStart`, `onEnd`, `onStartEnd`
12//! - Return value logging: `retVal`
13//! - Conflict detection and helpful error messages
14//!
15//! ## Examples
16//!
17//! ```rust
18//! use funlog::funlog;
19//!
20//! #[funlog(debug, all, retVal)]
21//! fn calculate(x: i32, y: i32) -> i32 {
22//!     x + y
23//! }
24//!
25//! #[funlog(info, params(name), onStart)]
26//! fn greet(name: &str, age: u32) {
27//!     println!("Hello, {}!", name);
28//! }
29//! ```
30
31mod config;
32mod config_builder;
33mod error;
34mod generics_item_fn;
35mod log_template;
36mod output;
37
38use config_builder::ConfigBuilder;
39use generics_item_fn::GenericsFn;
40use proc_macro::TokenStream;
41use syn::punctuated::Punctuated;
42use syn::token::Comma;
43use syn::Meta;
44use syn::{parse_macro_input, ItemFn};
45
46/// A procedural macro attribute for adding automatic logging to functions.
47///
48/// This macro generates logging code that can output function entry, exit,
49/// parameters, and return values based on the provided configuration.
50///
51/// # Arguments
52///
53/// The macro accepts various configuration options:
54///
55/// ## Log Levels
56/// - `print` - Use `println!` for output (default)
57/// - `trace` - Use `log::trace!`
58/// - `debug` - Use `log::debug!`
59/// - `info` - Use `log::info!`
60/// - `warn` - Use `log::warn!`
61/// - `error` - Use `log::error!`
62///
63/// ## Parameter Control
64/// - `all` - Log all function parameters (default)
65/// - `none` - Log no parameters
66/// - `params(param1, param2, ...)` - Log specific parameters
67///
68/// ## Position Control
69/// - `onStart` - Log only at function entry
70/// - `onEnd` - Log only at function exit
71/// - `onStartEnd` - Log at both entry and exit (default)
72///
73/// ## Return Value
74/// - `retVal` - Include return value in logging
75///
76/// # Examples
77///
78/// ```rust
79/// use funlog::funlog;
80///
81/// // Basic usage with debug logging and all parameters
82/// #[funlog(debug, all)]
83/// fn add(x: i32, y: i32) -> i32 {
84///     x + y
85/// }
86///
87/// // Log specific parameters only at function start
88/// #[funlog(info, params(name), onStart)]
89/// fn greet(name: &str, age: u32) {
90///     println!("Hello, {}!", name);
91/// }
92///
93/// // Include return value in logging
94/// #[funlog(debug, all, retVal)]
95/// fn multiply(x: i32, y: i32) -> i32 {
96///     x * y
97/// }
98///
99/// // No parameter logging, only function entry/exit
100/// #[funlog(trace, none)]
101/// fn process_data() {
102///     // processing logic
103/// }
104/// ```
105///
106/// # Errors
107///
108/// The macro will produce compile-time errors for:
109/// - Conflicting options (e.g., `debug, info`)
110/// - Invalid parameter names
111/// - Invalid attribute names (with suggestions)
112/// - Incorrect syntax
113///
114/// # Note
115///
116/// The macro only generates logging code in debug builds. In release builds,
117/// the original function is returned unchanged for optimal performance.
118#[proc_macro_attribute]
119pub fn funlog(args: TokenStream, item: TokenStream) -> TokenStream {
120    let is_debug = cfg!(debug_assertions);
121
122    // when not debug, just return the original function
123    if !is_debug {
124        return item;
125    }
126
127    let func = parse_macro_input!(item as ItemFn);
128    let func = GenericsFn::from(func);
129    let attr_meta: Punctuated<Meta, Comma> =
130        parse_macro_input!(args with Punctuated::<Meta, Comma>::parse_terminated);
131    match ConfigBuilder::from(attr_meta, func) {
132        Ok(config_builder) => match config_builder.build() {
133            Ok(config) => {
134                let output = config.to_output();
135                output.into()
136            }
137            Err(e) => {
138                let syn_error: syn::Error = e.into();
139                syn_error.into_compile_error().into()
140            }
141        },
142        Err(e) => {
143            let syn_error: syn::Error = e.into();
144            syn_error.into_compile_error().into()
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    use syn::{parse_quote, ItemFn};
154
155    #[test]
156    fn test_funlog_macro_exists() {
157        // This test ensures the macro is properly exported
158        // We can't easily test the macro directly in unit tests,
159        // but we can verify the supporting functions work
160
161        let func: ItemFn = parse_quote! {
162            fn test_func(x: i32) -> i32 { x + 1 }
163        };
164
165        let generics_fn = GenericsFn::from(func);
166        assert_eq!(generics_fn.sig.ident.to_string(), "test_func");
167    }
168
169    #[test]
170    fn test_debug_build_behavior() {
171        // In debug builds, the macro should process the function
172        // In release builds, it should return the original
173
174        // We can test this by checking the cfg! macro
175        let is_debug = cfg!(debug_assertions);
176
177        // This test documents the expected behavior
178        if is_debug {
179            // In debug builds, funlog should process the function
180            // Note: This is a documentation test, not an assertion
181        } else {
182            // In release builds, funlog should return original function
183            // Note: This is a documentation test, not an assertion
184        }
185    }
186
187    #[test]
188    fn test_integration_with_config_builder() {
189        let func: ItemFn = parse_quote! {
190            pub fn integration_test(x: i32, y: String) -> i32 {
191                x + 1
192            }
193        };
194
195        let generics_fn = GenericsFn::from(func);
196        let meta_list: Punctuated<Meta, Comma> = parse_quote! { debug, all, retVal };
197
198        let config_builder = ConfigBuilder::from(meta_list, generics_fn);
199        assert!(config_builder.is_ok());
200
201        let config = config_builder.unwrap().build();
202        assert!(config.is_ok());
203
204        let output = config.unwrap().to_output();
205        assert!(!output.inner_func.is_empty());
206    }
207
208    #[test]
209    fn test_error_handling_integration() {
210        let func: ItemFn = parse_quote! {
211            fn error_test() {}
212        };
213
214        let generics_fn = GenericsFn::from(func);
215
216        // Test with conflicting options
217        let meta_list: Punctuated<Meta, Comma> = parse_quote! { debug, info };
218
219        let result = ConfigBuilder::from(meta_list, generics_fn);
220        assert!(result.is_err());
221
222        // Verify it's the expected error type
223        match result.unwrap_err() {
224            error::ConfigError::ConflictingOptions { .. } => {
225                // Expected error type
226            }
227            _ => panic!("Expected ConflictingOptions error"),
228        }
229    }
230}