1#![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#[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#[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#[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}