Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-metric/nativelink-metric-macro-derive/src/lib.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::panic;
16
17
use proc_macro::TokenStream;
18
use quote::{ToTokens, format_ident, quote};
19
use syn::parse::{Parse, ParseStream};
20
use syn::{
21
    Attribute, DeriveInput, Ident, ImplGenerics, LitStr, TypeGenerics, WhereClause,
22
    parse_macro_input,
23
};
24
25
/// Holds the type of group for the metric. For example, if a metric
26
/// has no group it'll be `None`, if it has a static group name it'll
27
/// be `StaticGroupName(name_of_metric)`.
28
#[derive(Default, Debug)]
29
enum GroupType {
30
    #[default]
31
    None,
32
    StaticGroupName(Ident),
33
}
34
35
impl Parse for GroupType {
36
82
    fn parse(input: ParseStream) -> syn::Result<Self> {
37
82
        if input.is_empty() {
38
0
            return Ok(Self::None);
39
82
        }
40
82
        let group_str: LitStr = input.parse()
?0
;
41
82
        let group = format_ident!("{}", group_str.value());
42
82
        Ok(Self::StaticGroupName(group))
43
82
    }
44
}
45
46
impl ToTokens for GroupType {
47
449
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
48
449
        match self {
49
            Self::None => {
50
367
                quote! { "" }
51
            }
52
82
            Self::StaticGroupName(group) => quote! { stringify!(#group) },
53
        }
54
449
        .to_tokens(tokens);
55
449
    }
56
}
57
58
/// Holds the type of the metric. If the metric was not specified
59
/// it'll be `Default`, which will try to resolve the type from the
60
/// [`MetricsComponent::publish()`] method that got executed based on
61
/// the type of the field.
62
#[derive(Debug)]
63
enum MetricKind {
64
    Default,
65
    Counter,
66
    String,
67
    Component,
68
}
69
70
impl Parse for MetricKind {
71
0
    fn parse(input: ParseStream) -> syn::Result<Self> {
72
0
        let kind_str: LitStr = input.parse()?;
73
0
        match kind_str.value().as_str() {
74
0
            "counter" => Ok(Self::Counter),
75
0
            "string" => Ok(Self::String),
76
0
            "component" => Ok(Self::Component),
77
0
            "default" => Ok(Self::Default),
78
0
            _ => Err(syn::Error::new(kind_str.span(), "Invalid metric type")),
79
        }
80
0
    }
81
}
82
83
impl ToTokens for MetricKind {
84
449
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
85
449
        match self {
86
0
            Self::Counter => quote! { ::nativelink_metric::MetricKind::Counter },
87
0
            Self::String => quote! { ::nativelink_metric::MetricKind::String },
88
0
            Self::Component => quote! { ::nativelink_metric::MetricKind::Component },
89
449
            Self::Default => quote! { ::nativelink_metric::MetricKind::Default },
90
        }
91
449
        .to_tokens(tokens);
92
449
    }
93
}
94
95
/// Holds general information about a specific field that is to be published.
96
struct MetricFieldMetaData<'a> {
97
    field_name: &'a Ident,
98
    metric_kind: MetricKind,
99
    help: Option<LitStr>,
100
    group: GroupType,
101
    handler: Option<syn::ExprPath>,
102
}
103
104
impl<'a> MetricFieldMetaData<'a> {
105
449
    fn try_from(field_name: &'a Ident, attr: &Attribute) -> syn::Result<Self> {
106
449
        let mut result = MetricFieldMetaData {
107
449
            field_name,
108
449
            metric_kind: MetricKind::Default,
109
449
            help: None,
110
449
            group: GroupType::None,
111
449
            handler: None,
112
449
        };
113
        // If the attribute is just a path, it has no args, so use defaults.
114
449
        if let syn::Meta::Path(_) = attr.meta {
115
22
            return Ok(result);
116
427
        }
117
429
        
attr427
.
parse_args_with427
(
syn::meta::parser427
(|meta| {
118
429
            if meta.path.is_ident("help") {
119
347
                result.help = meta.value()
?0
.parse()
?0
;
120
82
            } else if meta.path.is_ident("kind") {
121
0
                result.metric_kind = meta.value()?.parse()?;
122
82
            } else if meta.path.is_ident("group") {
123
82
                result.group = meta.value()
?0
.parse()
?0
;
124
0
            } else if meta.path.is_ident("handler") {
125
0
                result.handler = Some(meta.value()?.parse()?);
126
0
            }
127
429
            Ok(())
128
429
        }))
?0
;
129
427
        Ok(result)
130
449
    }
131
}
132
133
/// Holds the template information about the struct. This is needed
134
/// to create the `MetricsComponent` impl.
135
struct Generics<'a> {
136
    implementation: ImplGenerics<'a>,
137
    ty: TypeGenerics<'a>,
138
    where_clause: Option<&'a WhereClause>,
139
}
140
141
/// Holds metadata about the struct that is having `MetricsComponent`
142
/// implemented.
143
struct MetricStruct<'a> {
144
    name: &'a Ident,
145
    metric_fields: Vec<MetricFieldMetaData<'a>>,
146
    generics: Generics<'a>,
147
}
148
149
impl ToTokens for MetricStruct<'_> {
150
135
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
151
135
        let name = &self.name;
152
135
        let impl_generics = &self.generics.implementation;
153
135
        let ty_generics = &self.generics.ty;
154
135
        let where_clause = &self.generics.where_clause;
155
156
449
        let 
metric_fields135
=
self.metric_fields.iter()135
.
map135
(|field| {
157
449
            let field_name = &field.field_name;
158
449
            let group = &field.group;
159
160
449
            let help = field
161
449
                .help
162
449
                .as_ref()
163
449
                .map_or_else(|| 
quote!102
{ ""
}102
, |help|
quote!347
{ #help
}347
);
164
165
449
            let value = field.handler.as_ref().map_or_else(
166
449
                || quote! { &self.#field_name },
167
0
                |handler| quote! { &#handler(&self.#field_name) },
168
            );
169
170
449
            let metric_kind = &field.metric_kind;
171
449
            quote! {
172
                ::nativelink_metric::publish!(
173
                    stringify!(#field_name),
174
                    #value,
175
                    #metric_kind,
176
                    #help,
177
                    #group
178
                );
179
            }
180
449
        });
181
135
        quote! {
182
            impl #impl_generics ::nativelink_metric::MetricsComponent for #name #ty_generics #where_clause {
183
                fn publish(&self, kind: ::nativelink_metric::MetricKind, field_metadata: ::nativelink_metric::MetricFieldData) -> Result<::nativelink_metric::MetricPublishKnownKindData, ::nativelink_metric::Error> {
184
                    #( #metric_fields )*
185
                    Ok(::nativelink_metric::MetricPublishKnownKindData::Component)
186
                }
187
            }
188
135
        }.to_tokens(tokens);
189
135
    }
190
}
191
192
#[proc_macro_derive(MetricsComponent, attributes(metric))]
193
135
pub fn metrics_component_derive(input: TokenStream) -> TokenStream {
194
135
    let input = parse_macro_input!(input as DeriveInput);
195
135
    let syn::Data::Struct(data) = &input.data else {
196
0
        panic!("MetricsComponent can only be derived for structs")
197
    };
198
199
135
    let mut metric_fields = vec![];
200
135
    match &data.fields {
201
135
        syn::Fields::Named(fields) => {
202
893
            
fields.named135
.
iter135
().
for_each135
(|field| {
203
1.12k
                
field.attrs.iter()893
.
for_each893
(|attr| {
204
1.12k
                    if attr.path().is_ident("metric") {
205
449
                        metric_fields.push(
206
449
                            MetricFieldMetaData::try_from(field.ident.as_ref().unwrap(), attr)
207
449
                                .unwrap(),
208
449
                        );
209
679
                    }
210
1.12k
                });
211
893
            });
212
        }
213
        syn::Fields::Unnamed(_) => {
214
0
            panic!("Unnamed fields are not supported");
215
        }
216
        syn::Fields::Unit => {
217
0
            panic!("Unit structs are not supported");
218
        }
219
    }
220
221
135
    let (implementation, ty, where_clause) = input.generics.split_for_impl();
222
135
    let metrics_struct = MetricStruct {
223
135
        name: &input.ident,
224
135
        metric_fields,
225
135
        generics: Generics {
226
135
            implementation,
227
135
            ty,
228
135
            where_clause,
229
135
        },
230
135
    };
231
    // This line is intentionally left here to make debugging
232
    // easier. If you want to see the output of the macro, just
233
    // uncomment this line and run the tests.
234
    // panic!("{}", quote! { #metrics_struct });
235
135
    TokenStream::from(quote! { #metrics_struct })
236
135
}