captains_log_helper/
lib.rs

1#![recursion_limit = "128"]
2use proc_macro2::Span;
3use quote::{quote, ToTokens};
4use syn::{
5    parse_macro_input, spanned::Spanned, token, Expr, ExprBlock, ExprClosure,
6    ItemFn, Result, ReturnType, LitStr, Ident, Signature, FnArg, Pat, PatType,
7};
8use syn::parse::{Parse, ParseStream};
9
10#[derive(Default, Debug)]
11struct Args {
12    level: Option<String>,
13}
14
15impl Parse for Args {
16    fn parse(input: ParseStream) -> syn::Result::<Self> {
17        let mut args = Self::default();
18        while !input.is_empty() {
19            if input.peek(LitStr) {
20                // [logfn("warn")]
21                let level = input.parse::<LitStr>()?;
22                args.level = Some(level.value());
23            } else if input.peek(Ident) {
24                // [logfn(warn)]
25                let ident = input.parse::<Ident>()?;
26                if args.level.is_none() {
27                    args.level = Some(ident.to_string());
28                }
29            } else {
30                return Err(input.error("unexpected token"));
31            }
32        }
33        if let Some(v) = args.level.as_ref() {
34            match v.as_str() {
35                "trace" | "debug" | "info" | "warn" | "error"=>{},
36                _=> {
37                    return Err(input.error(format!("unexpected level value: {:?}", v)));
38                }
39            }
40        }
41        Ok(args)
42    }
43}
44
45
46fn make_closure(original: &ItemFn) -> ExprClosure {
47    let body = Box::new(Expr::Block(ExprBlock {
48        attrs: Default::default(),
49        label: Default::default(),
50        block: *original.block.clone(),
51    }));
52
53    ExprClosure {
54        attrs: Default::default(),
55        lifetimes: Default::default(),
56        constness: Default::default(),
57        movability: Default::default(),
58        asyncness: Default::default(),
59        capture: Some(token::Move { span: original.span() }),
60        or1_token: Default::default(),
61        inputs: Default::default(),
62        or2_token: Default::default(),
63        output: ReturnType::Default,
64        body,
65    }
66}
67
68fn replace_function_headers(original: ItemFn, new: &mut ItemFn) {
69    let block = new.block.clone();
70    *new = original;
71    new.block = block;
72}
73
74fn gen_arg_list(sig: &Signature) -> String {
75    let mut arg_list = String::new();
76    for (i, input) in sig.inputs.iter().enumerate() {
77        if i > 0 {
78            arg_list.push_str(", ");
79        }
80        match input {
81            FnArg::Typed(PatType { pat, .. }) => {
82                if let Pat::Ident(pat_ident) = &**pat {
83                    let ident = &pat_ident.ident;
84                    arg_list.push_str(&format!("{ident} = {{{ident}:?}}"));
85                }
86            }
87            FnArg::Receiver(_) => {
88                arg_list.push_str("self");
89            }
90        }
91    }
92    arg_list
93}
94
95fn generate_function(closure: &ExprClosure, args: Args, fn_name: String, sig: &Signature) -> Result<ItemFn> {
96    let level = args.level.unwrap_or("info".to_string());
97    let level = Ident::new(&level, Span::call_site());
98    let arg_list = gen_arg_list(sig);
99    let fmt_begin = format!("<<< {} ({}) enter <<<", fn_name, arg_list);
100    let fmt_end = format!(">>> {} return {{__ret_value:?}} >>>", fn_name);
101    let begin_expr = quote! {log::#level!(#fmt_begin); };
102    let end_expr = quote! {log::#level!(#fmt_end); };
103    let code = quote! {
104        fn temp() {
105            #begin_expr;
106            let __ret_value = (#closure)();
107            #end_expr;
108        }
109    };
110
111    syn::parse2(code)
112}
113
114/// Provide an proc_macro `#[logfn]` which log the function call begin and return,
115/// with argument list and return value.
116///
117/// # Examples
118///
119/// ``` rust
120///
121/// use captains_log::{recipe, logfn};
122/// use log::*;
123///
124/// let builder = recipe::raw_file_logger("/tmp", "log_test", log::Level::Debug).test();
125/// builder.build().expect("setup_log");
126///
127/// // default log level to be info
128/// #[logfn]
129/// fn foo() {
130///     info!("foo");
131///     bar(1, "bar arg");
132/// }
133///
134/// // you can change log level to warn
135/// #[logfn(warn)]
136/// fn bar(a: i32, s: &str) {
137///     info!("bar");
138/// }
139/// foo();
140/// ```
141///
142/// /tmp/log_test.log will have this content:
143///
144/// ``` text
145/// [2025-06-21 01:00:17.116049][INFO][test_logfn.rs:13] <<< foo () enter <<<
146/// [2025-06-21 01:00:17.116206][INFO][test_logfn.rs:15] foo
147/// [2025-06-21 01:00:17.116223][WARN][test_logfn.rs:19] <<< bar (a = 1, s = "bar arg") enter <<<
148/// [2025-06-21 01:00:17.116236][INFO][test_logfn.rs:21] bar
149/// [2025-06-21 01:00:17.116246][WARN][test_logfn.rs:19] >>> bar return () >>>
150/// [2025-06-21 01:00:17.116256][INFO][test_logfn.rs:13] >>> foo return () >>>
151/// ```
152///
153/// # Best practice with test suit
154///
155/// Nice to have `#[logfn]` used with retest.
156///
157/// * When you have large test suit, you want to know which logs belong to which test case.
158///
159/// * Sometimes your test crashes, you want to find the resposible test case.
160///
161/// ```
162/// use rstest::*;
163/// use log::*;
164/// use captains_log::*;
165///
166/// // A show case that setup() fixture will be called twice, before each test.
167/// // In order make logs available.
168/// #[logfn]
169/// #[fixture]
170/// fn setup() {
171///     let builder = recipe::raw_file_logger("/tmp", "log_rstest", log::Level::Debug).test();
172///     builder.build().expect("setup_log");
173/// }
174///
175/// #[logfn]
176/// #[rstest(file_size, case(0), case(1))]
177/// fn test_rstest_foo(setup: (), file_size: usize) {
178///     info!("do something111");
179/// }
180///
181/// #[logfn]
182/// #[rstest]
183/// fn test_rstest_bar(setup: ()) {
184///     info!("do something222");
185/// }
186///
187/// ```
188///
189/// After running the test with:
190/// `cargo test -- --test-threads=1`
191///
192/// /tmp/log_rstest.log will have this content:
193///
194/// ``` text
195/// [2025-06-21 00:39:37.091326][INFO][test_rstest.rs:11] >>> setup return () >>>
196/// [2025-06-21 00:39:37.091462][INFO][test_rstest.rs:27] <<< test_rstest_bar (setup = ()) enter <<<
197/// [2025-06-21 00:39:37.091493][INFO][test_rstest.rs:30] do something222
198/// [2025-06-21 00:39:37.091515][INFO][test_rstest.rs:27] >>> test_rstest_bar return () >>>
199/// [2025-06-21 00:39:37.091719][INFO][test_rstest.rs:11] <<< setup () enter <<<
200/// [2025-06-21 00:39:37.091826][INFO][test_rstest.rs:11] >>> setup return () >>>
201/// [2025-06-21 00:39:37.091844][INFO][test_rstest.rs:21] <<< test_rstest_foo (setup = (), file_size = 0) enter <<<
202/// [2025-06-21 00:39:37.091857][INFO][test_rstest.rs:24] do something111
203/// [2025-06-21 00:39:37.091868][INFO][test_rstest.rs:21] >>> test_rstest_foo return () >>>
204/// [2025-06-21 00:39:37.092063][INFO][test_rstest.rs:11] <<< setup () enter <<<
205/// [2025-06-21 00:39:37.092136][INFO][test_rstest.rs:11] >>> setup return () >>>
206/// [2025-06-21 00:39:37.092151][INFO][test_rstest.rs:21] <<< test_rstest_foo (setup = (), file_size = 1) enter <<<
207/// [2025-06-21 00:39:37.092163][INFO][test_rstest.rs:24] do something111
208/// [2025-06-21 00:39:37.092173][INFO][test_rstest.rs:21] >>> test_rstest_foo return () >>>
209/// ```
210#[proc_macro_attribute]
211pub fn logfn(
212    attr: proc_macro::TokenStream, item: proc_macro::TokenStream,
213) -> proc_macro::TokenStream {
214    let original_fn: ItemFn = parse_macro_input!(item as ItemFn);
215    let args = parse_macro_input!(attr as Args);
216    let fn_name = original_fn.sig.ident.to_string();
217    let closure = make_closure(&original_fn);
218    let mut new_fn =
219        generate_function(&closure, args, fn_name, &original_fn.sig).expect("Failed Generating Function");
220    replace_function_headers(original_fn, &mut new_fn);
221    new_fn.into_token_stream().into()
222}