print_run/
lib.rs

1//! # print-run
2//!
3//! A procedural macro crate for tracing Rust function and module execution — with style.
4//!
5//! `print-run` provides `#[print_run]` attribute macro that automatically logs when functions start and finish, with optional color, duration, nested indentation, timestamps and more.
6//!
7//! It’s ideal for skeletons, debugging, quick prototypes, educational examples, or even just fun visual feedback.
8//!
9//! ## Features
10//!
11//! - Supports standalone functions, `impl` blocks, and inline `mod`s
12//! - Logs entry and exit of functions
13//! - Draws visual call hierarchy (indents nested calls)
14//! - Measures and displays execution duration
15//! - Supports async functions
16//! - Optional color and timestamps
17//! - `msg!()` macro for indented, bold `println!`-style output
18//!
19//! ## Usage
20//!
21//! Apply `#[print_run]` to any function:
22//!
23//! ```rust
24//! use print_run::print_run;
25//!
26//! #[print_run]
27//! fn demo() {
28//!     msg!("Running...");
29//! }
30//! ```
31//!
32//! Use `#[print_run_defaults(...)]` to set global options for the crate:
33//!
34//! ```rust
35//! #[print_run_defaults(indent, colored)]
36//! ```
37//!
38//! #### For more, see the crate [README](https://github.com/HeyJ0e/print-run-rs).
39//!
40//! ## License
41//!
42//! Licensed under MIT.
43
44use nu_ansi_term::{Color, ansi::RESET};
45use proc_macro::TokenStream;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::{ToTokens, format_ident, quote, quote_spanned};
48use std::sync::{Once, OnceLock};
49use syn::{
50    Attribute, Error, FnArg, Ident, ImplItem, ImplItemFn, Item, ItemFn, ItemImpl, ItemMod, LitStr,
51    Result, Token, parse, parse_macro_input, parse_quote, spanned::Spanned,
52};
53
54#[allow(unused)]
55#[cfg(doctest)]
56mod helper;
57
58static IS_HELPER_MODULE_ADDED: Once = Once::new();
59static PRINT_RUN_DEFAULTS: OnceLock<PrintRunArgs> = OnceLock::new();
60
61/// Optional input flags
62#[derive(Clone, Debug, Default)]
63struct PrintRunArgs {
64    colored: Option<bool>,
65    duration: Option<bool>,
66    indent: Option<bool>,
67    skip: Option<bool>,
68    supress_labels: Option<bool>,
69    timestamps: Option<bool>,
70    __struct_prefix: Option<String>,
71}
72
73impl PrintRunArgs {
74    fn merge(&mut self, override_args: &PrintRunArgs) {
75        self.colored = override_args.colored.or(self.colored);
76        self.duration = override_args.duration.or(self.duration);
77        self.indent = override_args.indent.or(self.indent);
78        self.skip = override_args.skip.or(self.skip);
79        self.supress_labels = override_args.supress_labels.or(self.supress_labels);
80        self.timestamps = override_args.timestamps.or(self.timestamps);
81        self.__struct_prefix = override_args
82            .__struct_prefix
83            .clone()
84            .or_else(|| self.__struct_prefix.clone());
85    }
86
87    fn add_globals(&mut self) {
88        if let Some(glob) = get_print_run_defaults() {
89            if let Some(v) = glob.colored {
90                self.colored = Some(v);
91            }
92            if let Some(v) = glob.duration {
93                self.duration = Some(v);
94            }
95            if let Some(v) = glob.indent {
96                self.indent = Some(v);
97            }
98            if let Some(v) = glob.skip {
99                self.skip = Some(v);
100            }
101            if let Some(v) = glob.supress_labels {
102                self.supress_labels = Some(v);
103            }
104            if let Some(v) = glob.timestamps {
105                self.timestamps = Some(v);
106            }
107        }
108    }
109
110    fn to_attribute(&self) -> Attribute {
111        let arg_idents = self.to_idents();
112        let pre = self.__struct_prefix.as_ref().and_then(|p| Some(p.as_str()));
113
114        match (arg_idents.is_empty(), pre) {
115            (true, None) => parse_quote! {
116                #[print_run::print_run]
117            },
118            (true, Some(pre_val)) => parse_quote! {
119                #[print_run::print_run(__struct_prefix = #pre_val)]
120            },
121            (false, None) => parse_quote! {
122                #[print_run::print_run( #(#arg_idents),* )]
123            },
124            (false, Some(pre_val)) => parse_quote! {
125                #[print_run::print_run( #(#arg_idents),*, __struct_prefix = #pre_val )]
126            },
127        }
128    }
129
130    fn to_idents(&self) -> Vec<Ident> {
131        let mut result = Vec::new();
132
133        if self.colored == Some(true) {
134            result.push(format_ident!("colored"));
135        }
136        if self.duration == Some(true) {
137            result.push(format_ident!("duration"));
138        }
139        if self.indent == Some(true) {
140            result.push(format_ident!("indent"));
141        }
142        if self.skip == Some(true) {
143            result.push(format_ident!("skip"));
144        }
145        if self.supress_labels == Some(true) {
146            result.push(format_ident!("supress_labels"));
147        }
148        if self.timestamps == Some(true) {
149            result.push(format_ident!("timestamps"));
150        }
151
152        result
153    }
154}
155
156impl parse::Parse for PrintRunArgs {
157    fn parse(input: parse::ParseStream) -> Result<Self> {
158        let mut args = PrintRunArgs::default();
159
160        while !input.is_empty() {
161            let ident: Ident = input.parse()?;
162            let _ = input.parse::<Option<Token![,]>>(); // allow optional commas
163
164            match ident.to_string().as_str() {
165                "colored" => args.colored = Some(true),
166                "duration" => args.duration = Some(true),
167                "indent" => args.indent = Some(true),
168                "skip" => args.skip = Some(true),
169                "supress_labels" => args.supress_labels = Some(true),
170                "timestamps" => args.timestamps = Some(true),
171                "__struct_prefix" => {
172                    let _ = input.parse::<Option<Token![=]>>()?;
173                    let lit: LitStr = input.parse()?;
174                    args.__struct_prefix = Some(lit.value().to_string());
175                }
176                other => {
177                    return Err(Error::new(
178                        ident.span(),
179                        format!("Unknown attribute '{}'", other),
180                    ));
181                }
182            }
183        }
184
185        Ok(args)
186    }
187}
188
189macro_rules! or_else {
190    ($cond:expr, $true_:expr, $false_:expr) => {
191        if $cond { $true_ } else { $false_ }
192    };
193}
194
195macro_rules! or_nothing {
196    ($cond:expr, $true_:expr) => {
197        or_else!($cond, $true_, quote! {})
198    };
199}
200
201macro_rules! or_empty_str {
202    ($cond:expr, $true_:expr) => {
203        or_else!($cond, $true_, quote! { || "".to_string() })
204    };
205}
206
207macro_rules! colorize {
208    ($txt:expr, $col_name: ident) => {
209        format!("{}{}{}", Color::$col_name.prefix().to_string(), $txt, RESET)
210    };
211}
212
213macro_rules! colorize_fn {
214    ($color_name: ident) => {{
215        let color = Color::$color_name.prefix().to_string();
216        quote! {
217            |txt: String| format!("{}{}{}", #color, txt, #RESET)
218        }
219    }};
220    ($color_name: ident, "bold") => {{
221        let color = Color::$color_name.bold().prefix().to_string();
222        quote! {
223            |txt: String| format!("{}{}{}", #color, txt, #RESET)
224        }
225    }};
226}
227
228macro_rules! create_timestamp {
229    ($colored:expr) => {{
230        let colorize = or_else!(
231            $colored,
232            colorize_fn!(DarkGray),
233            quote! { |txt: String| txt }
234        );
235        quote! {
236            || {
237                let now = std::time::SystemTime::now();
238                let epoch = now
239                    .duration_since(std::time::UNIX_EPOCH)
240                    .expect("Time went backwards");
241
242                let total_secs = epoch.as_secs();
243                let millis = epoch.subsec_millis();
244
245                let hours = (total_secs / 3600) % 24;
246                let minutes = (total_secs / 60) % 60;
247                let seconds = total_secs % 60;
248                let ts = format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis);
249                let ts = {#colorize}(ts);
250                format!("{} ", ts) // don't colorize the separator
251            }
252        }
253    }};
254}
255
256macro_rules! create_duration {
257    ($colored:expr, $supress_labels:expr) => {{
258        let colorize = or_else!($colored, colorize_fn!(Green), quote! { |txt: String| txt });
259        let supress_labels = $supress_labels;
260        quote! {
261            |start: std::time::Instant| {
262                let elapsed = start.elapsed().as_nanos();
263                let dur =
264                    if elapsed < 1_000 {
265                        format!("{}ns", elapsed)
266                    } else if elapsed < 1_000_000 {
267                        format!("{:.2}µs", elapsed as f64 / 1_000.0)
268                    } else if elapsed < 1_000_000_000 {
269                        format!("{:.2}ms", elapsed as f64 / 1_000_000.0)
270                    } else {
271                        format!("{:.2}s", elapsed as f64 / 1_000_000_000.0)
272                    }
273                ;
274                let dur = {#colorize}(dur); // colorize the result only
275                if #supress_labels {
276                    format!("[{}]", dur)
277                } else {
278                    format!(" in {}", dur)
279                }
280            }
281        }
282    }};
283}
284
285macro_rules! create_indent {
286    ($val:expr, $ch:expr) => {{
287        let val = $val;
288        let ch = $ch;
289
290        // hack for doctest
291        let depth_path: syn::Expr = if cfg!(doctest) {
292            syn::parse_str("crate::helper::DEPTH").unwrap()
293        } else {
294            syn::parse_str("crate::__print_run_helper::DEPTH").unwrap()
295        };
296
297        quote! {
298            || {#depth_path}.with(|depth| {
299                let depth_val = *depth.borrow();
300                *depth.borrow_mut() = depth_val.saturating_add_signed(#val);
301                let depth_val= depth_val.saturating_add_signed((#val-1) / 2);
302                let spaces = "┆ ".repeat(depth_val);
303                format!("{}{} ", spaces, #ch)
304            })
305        }
306    }};
307}
308
309fn get_print_run_defaults() -> Option<&'static PrintRunArgs> {
310    PRINT_RUN_DEFAULTS.get()
311}
312
313/// Attribute macro to define global defaults for `#[print_run(...)]`.
314///
315/// This macro sets the default configuration for all `#[print_run]` invocations
316/// in the current crate. It can be overridden locally.
317///
318/// It allows you to avoid repeating common arguments like `colored`, `indent`, or `duration`
319/// across multiple annotated items.
320///
321/// > ⚠️ Due to current Rust limitations, this cannot be written as a crate-level attribute like `#![print_run_defaults(...)]`.
322/// > Instead, place it somewhere inside the crate as a regular attribute.
323///
324/// # Usage
325///
326/// ```rust
327/// use print_run::{print_run, print_run_defaults};
328///
329/// #[print_run_defaults(colored, indent, duration)]
330/// #[print_run] // inherits: colored, indent, duration
331/// pub fn foo() {
332///     msg!("Hello from one");
333/// }
334///
335/// #[print_run(timestamps)] // adds timestamps to colored, indent, duration
336/// pub fn bar() {
337///     msg!("Hello from two");
338/// }
339///
340/// # fn main() {}
341/// ```
342///
343/// # Available arguments
344///
345/// - `colored`: Enable ANSI color output
346/// - `duration`: Show elapsed time on function exit
347/// - `indent`: Show visual indentation based on call depth
348/// - `skip`: Skip instrumentation for the entire crate
349/// - `supress_labels`: Hide `starting` / `ended` labels
350/// - `timestamps`: Add timestamp to each printed line
351///
352/// # Notes
353///
354/// - Can be used **once per create** (typically at the top of a module)
355/// - Only applies to functions that annotated with `#[print_run(...)]`
356/// - Cannot be applied with `#![...]` syntax (Rust limitation)
357///
358/// # See also
359///
360/// - [`print_run`](macro@crate::print_run): For instrumenting individual functions/modules/impl blocks
361///
362#[proc_macro_attribute]
363pub fn print_run_defaults(attr: TokenStream, input: TokenStream) -> TokenStream {
364    let attr_clone = attr.clone();
365    let args = parse_macro_input!(attr as PrintRunArgs);
366
367    if PRINT_RUN_DEFAULTS.set(args).is_err() {
368        return Error::new_spanned(
369            TokenStream2::from(attr_clone),
370            "print run defaults already set",
371        )
372        .to_compile_error()
373        .into();
374    }
375
376    input
377}
378
379/// Procedural macro to automatically log function entry and exit.
380///
381/// This macro prints messages when a function or method begins and ends execution.
382/// It can display timestamps, durations, and indentation based on configuration flags.
383/// It also enables a `msg!()` macro inside annotated functions for indented logging.
384///
385/// # Features
386///
387/// - Logs function start and end messages
388/// - Supports optional ANSI color output
389/// - Shows nested indentation for call hierarchy
390/// - Supports async functions
391/// - Shows execution duration (auto scales to s/ms/µs/ns)
392/// - Optionally prints timestamps
393/// - Usable on functions, impl blocks, and inline modules
394///
395/// # Usage
396///
397/// ```
398/// use print_run::print_run;
399///
400/// #[print_run]
401/// fn my_func() {
402///     msg!("Doing something!");
403/// }
404///
405/// #[print_run(colored, duration, indent)]
406/// fn another() { }
407///
408/// # fn main() {}
409/// ```
410///
411/// # Available arguments
412///
413/// - `colored`: Enable ANSI color output
414/// - `duration`: Show elapsed time on function exit
415/// - `indent`: Show visual indentation based on call depth
416/// - `skip`: Skip instrumentation for this item
417/// - `supress_labels`: Hide `starting` / `ended` labels
418/// - `timestamps`: Add timestamp to each printed line
419///
420/// # Module-level usage
421///
422/// Can be used on inline `mod` blocks to apply the same instrumentation to all functions inside:
423///
424/// ```
425/// use print_run::print_run;
426///
427/// #[print_run(indent, duration)]
428/// mod my_mod {
429///     pub fn foo() {}
430///
431///     #[print_run(skip)]
432///     pub fn bar() {} // This will not be instrumented
433/// }
434///
435/// # fn main() {}
436/// ```
437///
438/// # Impl blocks
439///
440/// Apply to `impl` blocks to instrument all methods inside:
441///
442/// ```
443/// use print_run::print_run;
444///
445/// struct MyStruct;
446///
447/// #[print_run(indent)]
448/// impl MyStruct {
449///     fn method_1(&self) {}
450///     fn method_2(&self) {}
451/// }
452///
453/// # fn main() {}
454/// ```
455///
456/// # `msg!()` macro
457///
458/// Inside `#[print_run]` functions, you can use `msg!()` as an indented alternative to `println!()`.
459/// It aligns with current indentation and can help make logs clearer:
460///
461/// ```
462/// use print_run::print_run;
463///
464/// #[print_run(indent)]
465/// fn do_something() {
466///     msg!("Hello {}", 123);
467/// }
468///
469/// # fn main() {}
470/// ```
471///
472/// # Limitations
473///
474/// - Only **inline modules** (`mod mymod { ... }`) are supported
475/// - Procedural macros can't modify items across files
476/// - Crate-level defaults must be written as `#[print_run_defaults(...)]` (not `#![...]`)
477///
478#[proc_macro_attribute]
479pub fn print_run(attr: TokenStream, item: TokenStream) -> TokenStream {
480    let args = parse_macro_input!(attr as PrintRunArgs);
481
482    // Try parsing as a function
483    if let Ok(mut func) = parse::<ItemFn>(item.clone()) {
484        let new_args = extract_and_flatten_print_args(&args, &mut func.attrs);
485        return print_run_fn(new_args, func);
486    }
487
488    // Try parsing as a module
489    if let Ok(mut module) = parse::<ItemMod>(item.clone()) {
490        let new_args = extract_and_flatten_print_args(&args, &mut module.attrs);
491        return print_run_mod(new_args, module);
492    }
493
494    // Try parsing as an implementation
495    if let Ok(mut implementation) = parse::<ItemImpl>(item.clone()) {
496        let new_args = extract_and_flatten_print_args(&args, &mut implementation.attrs);
497        return print_run_impl(new_args, implementation);
498    }
499
500    // Unsupported item — return error
501    Error::new_spanned(
502        TokenStream2::from(item),
503        "#[print_run] can only be used on functions, implementations or inline modules",
504    )
505    .to_compile_error()
506    .into()
507}
508
509fn print_run_fn(mut args: PrintRunArgs, mut fn_item: ItemFn) -> TokenStream {
510    // Add global args
511    args.add_globals();
512
513    // Extract args and item attributes
514    let PrintRunArgs {
515        colored,
516        duration,
517        indent,
518        skip,
519        supress_labels,
520        timestamps,
521        __struct_prefix,
522    } = args;
523    let colored = colored == Some(true);
524    let duration = duration == Some(true);
525    let indent = indent == Some(true);
526    let skip = skip == Some(true);
527    let supress_labels = supress_labels == Some(true);
528    let timestamps = timestamps == Some(true);
529
530    if skip {
531        // Add use println as msg to prevent missing `msg` macro
532        let use_msg = parse_quote! { use std::println as msg; };
533        fn_item.block.stmts.insert(0, use_msg);
534        return fn_item.to_token_stream().into();
535    }
536
537    let ItemFn {
538        attrs,
539        vis,
540        sig,
541        block,
542    } = fn_item;
543
544    // Create name with prefix
545    let fn_name = sig.ident.to_string();
546    let prefix = __struct_prefix.unwrap_or("".into());
547    let fn_name = format!("{prefix}{fn_name}");
548
549    // Create labels
550    let start_label = or_else!(!supress_labels, "starting", "");
551    let end_label = or_else!(!supress_labels, "ended", "");
552
553    // Create start/end function names
554    let start = or_else!(colored, colorize!(fn_name.clone(), Yellow), fn_name.clone());
555    let end = or_else!(colored, colorize!(fn_name.clone(), Blue), fn_name.clone());
556
557    // Create timestamp creator closure
558    let create_timestamp_fn = or_empty_str!(timestamps, create_timestamp!(colored));
559
560    // Create duration creator closure
561    let duration_fn = or_else!(
562        duration,
563        create_duration!(colored, supress_labels),
564        quote! { |_| "".to_string() }
565    );
566
567    // Create indent creator closures
568    let indent_top = or_empty_str!(indent, create_indent!(1isize, "┌"));
569    let indent_bottom = or_empty_str!(indent, create_indent!(-1isize, "└"));
570    let indent_body = or_empty_str!(indent, create_indent!(0isize, ""));
571
572    // Create msg! macro
573    let colorize_msg = if colored {
574        colorize_fn!(White, "bold")
575    } else {
576        colorize_fn!(Default, "bold")
577    };
578    let msg_macro = quote! {
579        #[allow(unused)]
580        macro_rules! msg {
581            ($($arg:tt)*) => {{
582                let ts = {#create_timestamp_fn}();
583                let indent = {#indent_body}();
584                let msg = format!($($arg)*);
585                let msg = {#colorize_msg}(msg);
586                println!("{}{} {}", ts, indent, msg);
587            }};
588        }
589    };
590
591    // Wrap the original function body
592    let new_block = quote_spanned! (block.to_token_stream().span() =>
593        {
594            let ts = {#create_timestamp_fn}();
595            let start = std::time::Instant::now();
596            let indent = {#indent_top}();
597            println!("{}{}{} {}", ts, indent, #start, #start_label);
598            #msg_macro
599
600            let result = {
601                #block
602            };
603
604            let dur = {#duration_fn}(start);
605            let ts = {#create_timestamp_fn}();
606            let indent = {#indent_bottom}();
607            println!("{}{}{} {}{}", ts, indent, #end, #end_label, dur);
608            result
609        }
610    );
611
612    // Add helper module if needed
613    let helper_module = define_helper_module();
614
615    // Reconstruct the function
616    quote! {
617        #(#attrs)*
618        #vis #sig #new_block
619        #helper_module
620    }
621    .into()
622}
623
624fn print_run_mod(args: PrintRunArgs, mut module_item: ItemMod) -> TokenStream {
625    // Check if it's an inline module
626    let content = match module_item.content {
627        Some((_, ref mut items)) => items,
628        _ => {
629            return Error::new_spanned(
630                module_item.mod_token,
631                "`#[print_run]` only supports inline modules",
632            )
633            .to_compile_error()
634            .into();
635        }
636    };
637
638    // Add the macro attribute to all functions and struct methods in the module
639    for item in content {
640        match item {
641            // Normal functions
642            Item::Fn(func) => {
643                let new_args = extract_and_flatten_print_args(&args, &mut func.attrs);
644                func.attrs.push(new_args.to_attribute());
645            }
646            // Struct member functions
647            Item::Impl(item_impl) => {
648                let new_args = extract_and_flatten_print_args(&args, &mut item_impl.attrs);
649                item_impl.attrs.push(new_args.to_attribute());
650            }
651            _ => {}
652        }
653    }
654
655    // Add helper module if needed
656    let helper_module = define_helper_module();
657    let module_tokens = module_item.into_token_stream();
658
659    quote! { #module_tokens #helper_module }.into()
660}
661
662fn print_run_impl(args: PrintRunArgs, mut impl_item: ItemImpl) -> TokenStream {
663    // Get struct name
664    let ty_str = (&impl_item.self_ty).into_token_stream().to_string();
665
666    // Look for methods
667    for impl_item in &mut impl_item.items {
668        if let ImplItem::Fn(method) = impl_item {
669            let mut new_args = extract_and_flatten_print_args(&args, &mut method.attrs);
670            let is_static = is_static_method(&method);
671            let ty_str = ty_str.clone() + if is_static { "::" } else { "." };
672            new_args.__struct_prefix = Some(ty_str);
673            method.attrs.push(new_args.to_attribute());
674        }
675    }
676
677    // Add helper module if needed
678    let helper_module = define_helper_module();
679    let impl_tokens = impl_item.into_token_stream();
680
681    quote! { #impl_tokens #helper_module }.into()
682}
683
684fn extract_and_flatten_print_args(
685    parent: &PrintRunArgs,
686    attrs: &mut Vec<Attribute>,
687) -> PrintRunArgs {
688    // Get global defaults or empty args
689    let mut merged_args = get_print_run_defaults()
690        .as_deref()
691        .and_then(|a| Some(a.clone()))
692        .unwrap_or(PrintRunArgs::default());
693
694    // Keep only non-#[print_run] attrs
695    attrs.retain(|attr| {
696        if attr.path().is_ident("print_run") {
697            if let Ok(parsed) = attr.parse_args::<PrintRunArgs>() {
698                merged_args.merge(&parsed);
699            }
700            false // drop this attr
701        } else {
702            true // keep
703        }
704    });
705    merged_args.merge(parent);
706    merged_args.add_globals();
707
708    merged_args
709}
710
711fn define_helper_module() -> TokenStream2 {
712    // Define support module with DEPTH if it runs for the first time
713    let mut define = false;
714    IS_HELPER_MODULE_ADDED.call_once(|| define = true);
715    or_nothing!(
716        define,
717        quote! {
718            #[doc(hidden)]
719            #[allow(unused)]
720            pub(crate) mod __print_run_helper {
721                use std::cell::RefCell;
722                thread_local! {
723                    pub static DEPTH: RefCell<usize> = RefCell::new(0);
724                }
725            }
726        }
727    )
728}
729
730fn is_static_method(method: &ImplItemFn) -> bool {
731    match method.sig.inputs.first() {
732        Some(FnArg::Receiver(_)) => false, // instance method
733        Some(FnArg::Typed(_)) => true,     // static method (no self)
734        None => true,                      // no args = static
735    }
736}