kexplain/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use proc_macro2::{Ident, Span};
4use quote::{quote, ToTokens};
5use syn::{parse_macro_input, parse_quote, ItemFn};
6
7/// Unwrap the Option value or break.
8macro_rules! or_continue {
9    ( $wrapper:expr ) => {
10        match $wrapper {
11            Some(v) => v,
12            None => continue,
13        }
14    };
15}
16
17fn has_attr(attrs: &[syn::Attribute], attr_name: &str) -> bool {
18    attrs.iter().any(|a| {
19        a.parse_meta()
20            .ok()
21            .map(|meta| meta.path().is_ident(attr_name))
22            .unwrap_or(false)
23    })
24}
25
26fn has_skip_attr(attrs: &[syn::Attribute]) -> bool {
27    has_attr(attrs, "skip")
28}
29
30fn has_no_expr_attr(attrs: &[syn::Attribute]) -> bool {
31    has_attr(attrs, "no_expr")
32}
33
34fn find_ident(pat: &syn::Pat) -> Option<&Ident> {
35    match pat {
36        syn::Pat::Ident(pat_ident) => Some(&pat_ident.ident),
37        _ => None,
38    }
39}
40
41#[proc_macro_attribute]
42/// ```
43/// use kexplain::explain;
44/// 
45/// #[explain]
46/// fn foo(a: u32, b: f64) -> u32 {
47///     let _x = a * b as u32;
48///     #[no_expr]
49///     let x = a * b as u32;
50///     #[skip]
51///     let _y = a * b as u32;
52///     x * 3
53/// }
54/// 
55/// struct Foo;
56/// 
57/// impl Foo {
58///     #[explain]
59///     fn bar(&self, a: u32, b: f64) -> u32 {
60///         let _x = a * b as u32;
61///         #[no_expr]
62///         let x = a * b as u32;
63///         #[skip]
64///         let _y = a * b as u32;
65///         x * 3
66///     }
67/// }
68/// 
69/// fn main() {
70///     assert_eq!(6, foo(1, 2.));
71///     assert_eq!(6, foo_explain(1, 2., |name, expr, value| {
72///         println!("{name} {expr:?} {value}");
73///     }));
74///     assert_eq!(6, Foo.bar(1, 2.));
75///     assert_eq!(6, Foo.bar_explain(1, 2., |name, expr, value| {
76///         println!("{name} {expr:?} {value}");
77///     }));
78/// }
79/// ```
80///
81/// Example stdout:
82/// ```text
83/// STDOUT:
84/// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
85/// a None 1
86/// b None 2
87/// _x Some("a * b as u32") 2
88/// x None 2
89///  None 6
90/// a None 1
91/// b None 2
92/// _x Some("a * b as u32") 2
93/// x None 2
94///  None 6
95/// ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
96/// ```
97///
98/// See the `tests` for more examples.
99pub fn explain(_attr: TokenStream, item: TokenStream) -> TokenStream {
100    let mut function = parse_macro_input!(item as ItemFn);
101    let mut new_function = function.clone();
102
103    // TODO wish I could use Span::def_site() but needs nightly
104    let callback = Ident::new("callback", Span::call_site());
105    let callback_arg: syn::FnArg = parse_quote! {
106        mut #callback: impl FnMut(&str, Option<&str>, &dyn std::fmt::Display)
107    };
108
109    new_function.sig.inputs.push(callback_arg);
110
111    // TODO wish I could use Span::def_site() but needs nightly
112    new_function.sig.ident = Ident::new(
113        &format!("{}_explain", function.sig.ident),
114        Span::call_site(),
115    );
116
117    let new_body = &mut new_function.block;
118    new_body.stmts.clear();
119    for arg in function.sig.inputs.iter() {
120        match arg {
121            syn::FnArg::Typed(pattype) if !has_skip_attr(&pattype.attrs) => {
122                let ident = or_continue!(find_ident(&pattype.pat));
123                let ident_str = ident.to_string();
124                let ident_str = ident_str.as_str();
125                new_body.stmts.push(parse_quote! {
126                    #callback(#ident_str, None, &#ident);
127                });
128            }
129            syn::FnArg::Receiver(_receiver) => (),
130            syn::FnArg::Typed(_) => (),
131        }
132    }
133    for stmt in function.block.stmts.iter_mut() {
134        match stmt {
135            syn::Stmt::Local(local) => {
136                let should_skip = has_skip_attr(&local.attrs);
137                let skip_expression = has_no_expr_attr(&local.attrs);
138                local.attrs.clear();
139                new_body.stmts.push(syn::Stmt::Local(local.clone()));
140                if should_skip {
141                    continue;
142                }
143                let expr = &or_continue!(local.init.as_ref()).1;
144                let ident = or_continue!(find_ident(&local.pat));
145                let ident_str = ident.to_string();
146                let ident_str = ident_str.as_str();
147                let expr_str = expr.to_token_stream().to_string();
148                let expr_str = expr_str.as_str();
149                let expr_expr: syn::Expr = if skip_expression {
150                    parse_quote! { None }
151                } else {
152                    parse_quote! { Some(#expr_str) }
153                };
154                new_body.stmts.push(parse_quote! {
155                    #callback(#ident_str, #expr_expr, &#ident);
156                });
157            }
158            // syn::Stmt::Item(_item) => (),
159            // syn::Stmt::Expr(_expr) => (),
160            // syn::Stmt::Semi(_expr, _semi) => (),
161            _ => {
162                new_body.stmts.push(stmt.clone());
163            }
164        }
165    }
166
167    *new_body = parse_quote! {
168        {
169            let result = #new_body;
170            #callback("", None, &result);
171            result
172        }
173    };
174
175    (quote! {
176        #function
177        #new_function
178    })
179    .into()
180}