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}