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    function.block = Box::new(syn::parse_quote!({
58        let mut __dbg_frame = #dbgflow::runtime::TraceFrame::enter(
59            #dbgflow::FunctionMeta {
60                id: concat!(module_path!(), "::", stringify!(#ident)),
61                label: #label,
62                module_path: module_path!(),
63                file: file!(),
64                line: line!(),
65                source: #source,
66            },
67            vec![#(#argument_values),*],
68        );
69        let __dbg_result = { #block };
70        __dbg_frame.finish_return(&__dbg_result);
71        __dbg_result
72    }));
73
74    quote!(#function).into()
75}
76
77/// Marks a struct or enum as a UI-visible data node.
78///
79/// Types annotated with `#[ui_debug]` implement `dbgflow::UiDebugValue` and can
80/// emit snapshots with `value.emit_snapshot("label")`.
81///
82/// Optional arguments:
83/// - `name = "..."` overrides the label shown in the UI.
84#[proc_macro_attribute]
85pub fn ui_debug(attr: TokenStream, item: TokenStream) -> TokenStream {
86    let options = parse_macro_input!(attr as MacroOptions);
87
88    let item = parse_macro_input!(item as Item);
89    match item {
90        Item::Struct(item_struct) => expand_struct(item_struct, options).into(),
91        Item::Enum(item_enum) => expand_enum(item_enum, options).into(),
92        _ => syn::Error::new(
93            proc_macro2::Span::call_site(),
94            "#[ui_debug] supports structs and enums only",
95        )
96        .to_compile_error()
97        .into(),
98    }
99}
100
101/// Wraps a test so it becomes a persisted debugger session.
102///
103/// The macro initializes a fresh session, records test start and finish events,
104/// persists the session if `DBG_SESSION_DIR` is set, and rethrows panics so the
105/// underlying test outcome remains unchanged.
106///
107/// Optional arguments:
108/// - `name = "..."` overrides the test node label shown in the UI.
109#[proc_macro_attribute]
110pub fn dbg_test(attr: TokenStream, item: TokenStream) -> TokenStream {
111    let options = parse_macro_input!(attr as MacroOptions);
112
113    let mut function = parse_macro_input!(item as ItemFn);
114    let ident = function.sig.ident.clone();
115    let dbgflow = dbgflow_crate_path();
116    let label = options.label_or(&ident);
117
118    if function.sig.asyncness.is_some() {
119        return syn::Error::new_spanned(
120            &function.sig.ident,
121            "#[dbg_test] does not support async tests yet",
122        )
123        .to_compile_error()
124        .into();
125    }
126
127    if !function
128        .attrs
129        .iter()
130        .any(|attr| attr.path().is_ident("test"))
131    {
132        function.attrs.push(syn::parse_quote!(#[test]));
133    }
134
135    let test_name = format!("{}", ident);
136    let block = &function.block;
137    function.block = Box::new(syn::parse_quote!({
138        let __dbg_test_name = concat!(module_path!(), "::", #test_name);
139        #dbgflow::init_session(format!("dbgflow test: {}", __dbg_test_name));
140        #dbgflow::runtime::record_test_started_latest_with_label(__dbg_test_name, #label);
141
142        let __dbg_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| #block));
143        match __dbg_result {
144            Ok(__dbg_value) => {
145                #dbgflow::runtime::record_test_passed_latest_with_label(__dbg_test_name, #label);
146                let _ = #dbgflow::persist_session_from_env(__dbg_test_name);
147                __dbg_value
148            }
149            Err(__dbg_panic) => {
150                #dbgflow::runtime::record_test_failed_latest_with_label(
151                    __dbg_test_name,
152                    #label,
153                    #dbgflow::panic_message(&*__dbg_panic),
154                );
155                let _ = #dbgflow::persist_session_from_env(__dbg_test_name);
156                ::std::panic::resume_unwind(__dbg_panic)
157            }
158        }
159    }));
160
161    quote!(#function).into()
162}
163
164fn expand_struct(mut item: ItemStruct, options: MacroOptions) -> proc_macro2::TokenStream {
165    let source = formatted_struct_source(&item);
166    maybe_add_debug_derive(&mut item.attrs);
167    let ident = &item.ident;
168    let dbgflow = dbgflow_crate_path();
169    let label = options.label_or(ident);
170
171    quote! {
172        #item
173
174        impl #dbgflow::UiDebugValue for #ident {
175            fn ui_debug_type_meta() -> #dbgflow::TypeMeta {
176                #dbgflow::TypeMeta {
177                    id: concat!(module_path!(), "::", stringify!(#ident)),
178                    label: #label,
179                    module_path: module_path!(),
180                    file: file!(),
181                    line: line!(),
182                    source: #source,
183                }
184            }
185        }
186    }
187}
188
189fn expand_enum(mut item: ItemEnum, options: MacroOptions) -> proc_macro2::TokenStream {
190    let source = formatted_enum_source(&item);
191    maybe_add_debug_derive(&mut item.attrs);
192    let ident = &item.ident;
193    let dbgflow = dbgflow_crate_path();
194    let label = options.label_or(ident);
195
196    quote! {
197        #item
198
199        impl #dbgflow::UiDebugValue for #ident {
200            fn ui_debug_type_meta() -> #dbgflow::TypeMeta {
201                #dbgflow::TypeMeta {
202                    id: concat!(module_path!(), "::", stringify!(#ident)),
203                    label: #label,
204                    module_path: module_path!(),
205                    file: file!(),
206                    line: line!(),
207                    source: #source,
208                }
209            }
210        }
211    }
212}
213
214fn maybe_add_debug_derive(attrs: &mut Vec<Attribute>) {
215    let has_debug = attrs.iter().any(|attr| {
216        attr.path().is_ident("derive") && attr.meta.to_token_stream().to_string().contains("Debug")
217    });
218
219    if !has_debug {
220        attrs.push(syn::parse_quote!(#[derive(Debug)]));
221    }
222}
223
224fn dbgflow_crate_path() -> proc_macro2::TokenStream {
225    match crate_name("dbgflow") {
226        Ok(FoundCrate::Itself) => quote!(crate),
227        Ok(FoundCrate::Name(name)) => {
228            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
229            quote!(::#ident)
230        }
231        Err(_) => quote!(::dbgflow),
232    }
233}
234
235#[derive(Default)]
236struct MacroOptions {
237    name: Option<LitStr>,
238}
239
240impl MacroOptions {
241    fn label_or(&self, fallback: &Ident) -> LitStr {
242        self.name
243            .clone()
244            .unwrap_or_else(|| LitStr::new(&fallback.to_string(), fallback.span()))
245    }
246}
247
248impl Parse for MacroOptions {
249    fn parse(input: ParseStream<'_>) -> Result<Self> {
250        if input.is_empty() {
251            return Ok(Self::default());
252        }
253
254        let key: Ident = input.parse()?;
255        input.parse::<Token![=]>()?;
256        let value: LitStr = input.parse()?;
257
258        if !input.is_empty() {
259            return Err(input.error("expected only `name = \"...\"`"));
260        }
261
262        if key != "name" {
263            return Err(syn::Error::new(
264                key.span(),
265                "supported options: `name = \"...\"`",
266            ));
267        }
268
269        Ok(Self { name: Some(value) })
270    }
271}
272
273fn formatted_function_source(function: &ItemFn) -> LitStr {
274    formatted_item_source(Item::Fn(function.clone()))
275}
276
277fn formatted_struct_source(item: &ItemStruct) -> LitStr {
278    formatted_item_source(Item::Struct(item.clone()))
279}
280
281fn formatted_enum_source(item: &ItemEnum) -> LitStr {
282    formatted_item_source(Item::Enum(item.clone()))
283}
284
285fn formatted_item_source(item: Item) -> LitStr {
286    let file = syn::File {
287        shebang: None,
288        attrs: Vec::new(),
289        items: vec![item],
290    };
291    let source = prettyplease::unparse(&file);
292    LitStr::new(&source, proc_macro2::Span::call_site())
293}