Skip to main content

dbgflow_macros/
lib.rs

1//! Procedural macros for the `dbgflow` graph debugger.
2//!
3//! This crate is usually consumed indirectly through the top-level `dbgflow`
4//! crate, which re-exports all macros.
5#![warn(missing_docs)]
6
7use proc_macro::TokenStream;
8use proc_macro_crate::{FoundCrate, crate_name};
9use quote::{ToTokens, quote};
10use syn::{
11    Attribute, Ident, Item, ItemEnum, ItemFn, ItemStruct, LitStr, Result, Token,
12    parse::{Parse, ParseStream},
13    parse_macro_input,
14};
15
16/// Marks a function as a traced execution node.
17///
18/// The generated code records function entry, argument previews, and the final
19/// return event into the active session.
20///
21/// Optional arguments:
22/// - `name = "..."` overrides the label shown in the UI.
23#[proc_macro_attribute]
24pub fn trace(attr: TokenStream, item: TokenStream) -> TokenStream {
25    let options = parse_macro_input!(attr as MacroOptions);
26
27    let mut function = parse_macro_input!(item as ItemFn);
28    let original_function = function.clone();
29    let ident = function.sig.ident.clone();
30    let dbgflow = dbgflow_crate_path();
31    let label = options.label_or(&ident);
32    let source = formatted_function_source(&original_function);
33
34    let argument_values = function.sig.inputs.iter().map(|arg| match arg {
35        syn::FnArg::Receiver(_) => {
36            quote! { #dbgflow::runtime::preview_argument("self", &self) }
37        }
38        syn::FnArg::Typed(pat_type) => {
39            let pat = &pat_type.pat;
40            let name = pat.to_token_stream().to_string();
41            match pat.as_ref() {
42                syn::Pat::Ident(pat_ident) => {
43                    let binding = &pat_ident.ident;
44                    quote! { #dbgflow::runtime::preview_argument(#name, &#binding) }
45                }
46                _ => quote! {
47                    #dbgflow::ValueSlot {
48                        name: #name.to_owned(),
49                        preview: "<non-ident pattern>".to_owned(),
50                    }
51                },
52            }
53        }
54    });
55
56    let block = &function.block;
57
58    if function.sig.asyncness.is_some() {
59        function.block = Box::new(syn::parse_quote!({
60            #dbgflow::runtime::trace_future(
61                #dbgflow::FunctionMeta {
62                    id: concat!(module_path!(), "::", stringify!(#ident)),
63                    label: #label,
64                    module_path: module_path!(),
65                    file: file!(),
66                    line: line!(),
67                    source: #source,
68                },
69                vec![#(#argument_values),*],
70                async move { #block }
71            ).await
72        }));
73    } else {
74        function.block = Box::new(syn::parse_quote!({
75            let mut __dbg_frame = #dbgflow::runtime::TraceFrame::enter(
76                #dbgflow::FunctionMeta {
77                    id: concat!(module_path!(), "::", stringify!(#ident)),
78                    label: #label,
79                    module_path: module_path!(),
80                    file: file!(),
81                    line: line!(),
82                    source: #source,
83                },
84                vec![#(#argument_values),*],
85            );
86            let __dbg_result = { #block };
87            __dbg_frame.finish_return(&__dbg_result);
88            __dbg_result
89        }));
90    }
91
92    quote!(#function).into()
93}
94
95/// Marks a struct or enum as a UI-visible data node.
96///
97/// Types annotated with `#[ui_debug]` implement `dbgflow::UiDebugValue` and can
98/// emit snapshots with `value.emit_snapshot("label")`.
99///
100/// Optional arguments:
101/// - `name = "..."` overrides the label shown in the UI.
102#[proc_macro_attribute]
103pub fn ui_debug(attr: TokenStream, item: TokenStream) -> TokenStream {
104    let options = parse_macro_input!(attr as MacroOptions);
105
106    let item = parse_macro_input!(item as Item);
107    match item {
108        Item::Struct(item_struct) => expand_struct(item_struct, options).into(),
109        Item::Enum(item_enum) => expand_enum(item_enum, options).into(),
110        _ => syn::Error::new(
111            proc_macro2::Span::call_site(),
112            "#[ui_debug] supports structs and enums only",
113        )
114        .to_compile_error()
115        .into(),
116    }
117}
118
119/// Wraps a test so it becomes a persisted debugger session.
120///
121/// The macro initializes a fresh session, records test start and finish events,
122/// persists the session if `DBG_SESSION_DIR` is set, and rethrows panics so the
123/// underlying test outcome remains unchanged.
124///
125/// Optional arguments:
126/// - `name = "..."` overrides the test node label shown in the UI.
127#[proc_macro_attribute]
128pub fn dbg_test(attr: TokenStream, item: TokenStream) -> TokenStream {
129    let options = parse_macro_input!(attr as MacroOptions);
130
131    let mut function = parse_macro_input!(item as ItemFn);
132    let ident = function.sig.ident.clone();
133    let dbgflow = dbgflow_crate_path();
134    let label = options.label_or(&ident);
135
136    if function.sig.asyncness.is_some() {
137        return syn::Error::new_spanned(
138            &function.sig.ident,
139            "#[dbg_test] does not support async tests yet",
140        )
141        .to_compile_error()
142        .into();
143    }
144
145    if !function
146        .attrs
147        .iter()
148        .any(|attr| attr.path().is_ident("test"))
149    {
150        function.attrs.push(syn::parse_quote!(#[test]));
151    }
152
153    let test_name = format!("{}", ident);
154    let block = &function.block;
155    function.block = Box::new(syn::parse_quote!({
156        let __dbg_test_name = concat!(module_path!(), "::", #test_name);
157        #dbgflow::init_session(format!("dbgflow test: {}", __dbg_test_name));
158        #dbgflow::runtime::record_test_started_latest_with_label(__dbg_test_name, #label);
159
160        let __dbg_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| #block));
161        match __dbg_result {
162            Ok(__dbg_value) => {
163                #dbgflow::runtime::record_test_passed_latest_with_label(__dbg_test_name, #label);
164                let _ = #dbgflow::persist_session_from_env(__dbg_test_name);
165                __dbg_value
166            }
167            Err(__dbg_panic) => {
168                #dbgflow::runtime::record_test_failed_latest_with_label(
169                    __dbg_test_name,
170                    #label,
171                    #dbgflow::panic_message(&*__dbg_panic),
172                );
173                let _ = #dbgflow::persist_session_from_env(__dbg_test_name);
174                ::std::panic::resume_unwind(__dbg_panic)
175            }
176        }
177    }));
178
179    quote!(#function).into()
180}
181
182fn expand_struct(mut item: ItemStruct, options: MacroOptions) -> proc_macro2::TokenStream {
183    let source = formatted_struct_source(&item);
184    maybe_add_debug_derive(&mut item.attrs);
185    let ident = &item.ident;
186    let dbgflow = dbgflow_crate_path();
187    let label = options.label_or(ident);
188
189    quote! {
190        #item
191
192        impl #dbgflow::UiDebugValue for #ident {
193            fn ui_debug_type_meta() -> #dbgflow::TypeMeta {
194                #dbgflow::TypeMeta {
195                    id: concat!(module_path!(), "::", stringify!(#ident)),
196                    label: #label,
197                    module_path: module_path!(),
198                    file: file!(),
199                    line: line!(),
200                    source: #source,
201                }
202            }
203        }
204    }
205}
206
207fn expand_enum(mut item: ItemEnum, options: MacroOptions) -> proc_macro2::TokenStream {
208    let source = formatted_enum_source(&item);
209    maybe_add_debug_derive(&mut item.attrs);
210    let ident = &item.ident;
211    let dbgflow = dbgflow_crate_path();
212    let label = options.label_or(ident);
213
214    quote! {
215        #item
216
217        impl #dbgflow::UiDebugValue for #ident {
218            fn ui_debug_type_meta() -> #dbgflow::TypeMeta {
219                #dbgflow::TypeMeta {
220                    id: concat!(module_path!(), "::", stringify!(#ident)),
221                    label: #label,
222                    module_path: module_path!(),
223                    file: file!(),
224                    line: line!(),
225                    source: #source,
226                }
227            }
228        }
229    }
230}
231
232fn maybe_add_debug_derive(attrs: &mut Vec<Attribute>) {
233    let has_debug = attrs.iter().any(|attr| {
234        attr.path().is_ident("derive") && attr.meta.to_token_stream().to_string().contains("Debug")
235    });
236
237    if !has_debug {
238        attrs.push(syn::parse_quote!(#[derive(Debug)]));
239    }
240}
241
242fn dbgflow_crate_path() -> proc_macro2::TokenStream {
243    match crate_name("dbgflow") {
244        Ok(FoundCrate::Itself) => quote!(crate),
245        Ok(FoundCrate::Name(name)) => {
246            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
247            quote!(::#ident)
248        }
249        Err(_) => quote!(::dbgflow),
250    }
251}
252
253#[derive(Default)]
254struct MacroOptions {
255    name: Option<LitStr>,
256}
257
258impl MacroOptions {
259    fn label_or(&self, fallback: &Ident) -> LitStr {
260        self.name
261            .clone()
262            .unwrap_or_else(|| LitStr::new(&fallback.to_string(), fallback.span()))
263    }
264}
265
266impl Parse for MacroOptions {
267    fn parse(input: ParseStream<'_>) -> Result<Self> {
268        if input.is_empty() {
269            return Ok(Self::default());
270        }
271
272        let key: Ident = input.parse()?;
273        input.parse::<Token![=]>()?;
274        let value: LitStr = input.parse()?;
275
276        if !input.is_empty() {
277            return Err(input.error("expected only `name = \"...\"`"));
278        }
279
280        if key != "name" {
281            return Err(syn::Error::new(
282                key.span(),
283                "supported options: `name = \"...\"`",
284            ));
285        }
286
287        Ok(Self { name: Some(value) })
288    }
289}
290
291fn formatted_function_source(function: &ItemFn) -> LitStr {
292    formatted_item_source(Item::Fn(function.clone()))
293}
294
295fn formatted_struct_source(item: &ItemStruct) -> LitStr {
296    formatted_item_source(Item::Struct(item.clone()))
297}
298
299fn formatted_enum_source(item: &ItemEnum) -> LitStr {
300    formatted_item_source(Item::Enum(item.clone()))
301}
302
303fn formatted_item_source(item: Item) -> LitStr {
304    let file = syn::File {
305        shebang: None,
306        attrs: Vec::new(),
307        items: vec![item],
308    };
309    let source = prettyplease::unparse(&file);
310    LitStr::new(&source, proc_macro2::Span::call_site())
311}