function_timer_macro/
lib.rs

1//! `time` macro. It can place on any function.
2use proc_macro::TokenStream;
3use proc_macro2::Span;
4use quote::quote;
5use syn::fold::Fold;
6use syn::parse::{Parse, ParseStream};
7use syn::spanned::Spanned;
8use syn::token::Impl;
9use syn::{
10    parse_macro_input, Attribute, Block, Expr, ExprBlock, Ident, ImplItem, ImplItemFn, ItemFn,
11    ItemImpl, LitStr, Meta, Stmt, Type,
12};
13
14mod custom_keywords {
15    syn::custom_keyword!(disable);
16}
17
18enum Name {
19    Literal(LitStr),
20    Ident(Ident),
21    Disable(custom_keywords::disable),
22}
23
24impl Name {
25    fn disable(&self) -> bool {
26        matches!(self, Name::Disable(_))
27    }
28
29    fn span(&self) -> Span {
30        match self {
31            Self::Literal(lit) => lit.span(),
32            Self::Ident(ident) => ident.span(),
33            Self::Disable(tok) => tok.span(),
34        }
35    }
36}
37
38struct MetricName {
39    struct_name: Option<String>,
40    name: Name,
41}
42
43impl MetricName {
44    /// Find if there's any override time attribute.
45    fn any_time_attribut(attributs: &[Attribute]) -> bool {
46        attributs.iter().any(|attr| {
47            if let Meta::Path(path) = &attr.meta {
48                path.segments.last().map(|i| i.ident.to_string()) == Some("time".to_string())
49            } else if let Meta::List(list) = &attr.meta {
50                list.path.segments.last().map(|i| i.ident.to_string()) == Some("time".to_string())
51            } else {
52                false
53            }
54        })
55    }
56
57    fn block_from(&self, block: Block, function_name: String) -> Block {
58        let metric_name = match &self.name {
59            Name::Literal(lit) => quote!(#lit),
60            Name::Ident(ident) => quote!(#ident),
61            // Early return the block as it shouldn't change (disable)
62            Name::Disable(_) => return block,
63        };
64        let st = self.struct_name.clone();
65
66        let macro_stmt = if let Some(st) = st {
67            quote!(
68                let _guard = function_timer::FunctionTimer::new(#metric_name, Some(#st), #function_name);
69            )
70        } else {
71            quote!(
72                let _guard = function_timer::FunctionTimer::new(#metric_name, None, #function_name);
73            )
74        };
75        let mut statements: Vec<Stmt> = Vec::with_capacity(2);
76
77        let macro_stmt: Stmt = syn::parse2(macro_stmt).expect("Can't parse token");
78        statements.push(macro_stmt);
79
80        statements.push(Stmt::Expr(
81            Expr::Block(ExprBlock {
82                attrs: vec![],
83                label: None,
84                block,
85            }),
86            None,
87        ));
88
89        Block {
90            brace_token: Default::default(),
91            stmts: statements,
92        }
93    }
94}
95
96impl Parse for MetricName {
97    fn parse(input: ParseStream) -> syn::Result<Self> {
98        let lookahead = input.lookahead1();
99        let name =
100            if lookahead.peek(custom_keywords::disable) {
101                Name::Disable(input.parse::<custom_keywords::disable>()?)
102            } else if lookahead.peek(LitStr) {
103                Name::Literal(input.parse()?)
104            } else {
105                Name::Ident(input.parse().map_err(|error| {
106                    syn::Error::new(error.span(), "Expected literal or identifier")
107                })?)
108            };
109
110        Ok(Self {
111            struct_name: None,
112            name,
113        })
114    }
115}
116
117impl Fold for MetricName {
118    fn fold_impl_item_fn(&mut self, i: ImplItemFn) -> ImplItemFn {
119        // If there's a time attribut, it overrides the impl one.
120        // This also allows handling to disable
121        if Self::any_time_attribut(&i.attrs) {
122            return i;
123        }
124
125        let mut result = i.clone();
126        let block = i.block;
127        let name = i.sig.ident.to_string();
128        let new_block = self.block_from(block, name);
129        result.block = new_block;
130
131        result
132    }
133
134    fn fold_item_fn(&mut self, i: ItemFn) -> ItemFn {
135        let block = *i.block;
136        let name = i.sig.ident.to_string();
137
138        let new_block = self.block_from(block, name);
139
140        ItemFn {
141            attrs: i.attrs,
142            vis: i.vis,
143            sig: i.sig,
144            block: Box::new(new_block),
145        }
146    }
147    fn fold_item_impl(&mut self, i: ItemImpl) -> ItemImpl {
148        let mut new_items: Vec<ImplItem> = Vec::with_capacity(i.items.len());
149        let mut result = i.clone();
150        if let Type::Path(p) = *i.self_ty {
151            self.struct_name = p.path.segments.last().map(|p| p.ident.to_string());
152        }
153        for item in i.items {
154            if let ImplItem::Fn(method) = item {
155                new_items.push(ImplItem::Fn(self.fold_impl_item_fn(method)));
156            } else {
157                new_items.push(item);
158            }
159        }
160        result.items = new_items;
161        result
162    }
163}
164
165enum ImplOrFn {
166    Function(ItemFn),
167    ImplStruct(ItemImpl),
168}
169
170impl ImplOrFn {
171    fn is_impl(&self) -> bool {
172        matches!(self, ImplOrFn::ImplStruct(_))
173    }
174}
175
176impl Parse for ImplOrFn {
177    fn parse(input: ParseStream) -> syn::Result<Self> {
178        let attrs = input.call(Attribute::parse_outer)?;
179        if input.peek(Impl) {
180            let mut item: ItemImpl = input.parse()?;
181            item.attrs = attrs;
182            Ok(Self::ImplStruct(item))
183        } else {
184            let mut item: ItemFn = input.parse()?;
185            item.attrs = attrs;
186            Ok(Self::Function(item))
187        }
188    }
189}
190
191/// Macro that time a function and emit a histogram metric using `metrics` crate
192/// ```norust
193/// #[time("metric_name")]
194/// ```
195/// This macro can be on a function.
196#[proc_macro_attribute]
197pub fn time(attr: TokenStream, item: TokenStream) -> TokenStream {
198    let mut args = parse_macro_input!(attr as MetricName);
199    let input = parse_macro_input!(item as ImplOrFn);
200
201    if args.name.disable() && input.is_impl() {
202        return syn::Error::new(args.name.span(), "You can't disable a whole impl block")
203            .into_compile_error()
204            .into();
205    }
206
207    match input {
208        ImplOrFn::Function(item_fn) => {
209            let output = args.fold_item_fn(item_fn);
210            TokenStream::from(quote!(#output))
211        }
212        ImplOrFn::ImplStruct(impl_struct) => {
213            let output = args.fold_item_impl(impl_struct);
214            TokenStream::from(quote!(#output))
215        }
216    }
217}