uv_macros/
lib.rs

1mod options_metadata;
2
3use proc_macro::TokenStream;
4use quote::{quote, quote_spanned};
5use syn::spanned::Spanned;
6use syn::{Attribute, DeriveInput, ImplItem, ItemImpl, LitStr, parse_macro_input};
7
8#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
9pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
10    let input = parse_macro_input!(input as DeriveInput);
11
12    options_metadata::derive_impl(input)
13        .unwrap_or_else(syn::Error::into_compile_error)
14        .into()
15}
16
17#[proc_macro_derive(CombineOptions)]
18pub fn derive_combine(input: TokenStream) -> TokenStream {
19    let input = parse_macro_input!(input as DeriveInput);
20    impl_combine(&input)
21}
22
23fn impl_combine(ast: &DeriveInput) -> TokenStream {
24    let name = &ast.ident;
25    let fields = if let syn::Data::Struct(syn::DataStruct {
26        fields: syn::Fields::Named(ref fields),
27        ..
28    }) = ast.data
29    {
30        &fields.named
31    } else {
32        unimplemented!();
33    };
34
35    let combines = fields.iter().map(|f| {
36        let name = &f.ident;
37        quote! {
38            #name: self.#name.combine(other.#name)
39        }
40    });
41
42    let stream = quote! {
43        impl crate::Combine for #name {
44            fn combine(self, other: #name) -> #name {
45                #name {
46                    #(#combines),*
47                }
48            }
49        }
50    };
51    stream.into()
52}
53
54fn get_doc_comment(attrs: &[Attribute]) -> String {
55    attrs
56        .iter()
57        .filter_map(|attr| {
58            if attr.path().is_ident("doc") {
59                if let syn::Meta::NameValue(meta) = &attr.meta {
60                    if let syn::Expr::Lit(expr) = &meta.value {
61                        if let syn::Lit::Str(str) = &expr.lit {
62                            return Some(str.value().trim().to_string());
63                        }
64                    }
65                }
66            }
67            None
68        })
69        .collect::<Vec<_>>()
70        .join("\n")
71}
72
73fn get_env_var_pattern_from_attr(attrs: &[Attribute]) -> Option<String> {
74    attrs
75        .iter()
76        .find(|attr| attr.path().is_ident("attr_env_var_pattern"))
77        .and_then(|attr| attr.parse_args::<LitStr>().ok())
78        .map(|lit_str| lit_str.value())
79}
80
81fn get_added_in(attrs: &[Attribute]) -> Option<String> {
82    attrs
83        .iter()
84        .find(|a| a.path().is_ident("attr_added_in"))
85        .and_then(|attr| attr.parse_args::<LitStr>().ok())
86        .map(|lit_str| lit_str.value())
87}
88
89fn is_hidden(attrs: &[Attribute]) -> bool {
90    attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
91}
92
93/// This attribute is used to generate environment variables metadata for [`uv_static::EnvVars`].
94#[proc_macro_attribute]
95pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
96    let ast = parse_macro_input!(input as ItemImpl);
97
98    let constants: Vec<_> = ast
99        .items
100        .iter()
101        .filter_map(|item| match item {
102            ImplItem::Const(item) if !is_hidden(&item.attrs) => {
103                let doc = get_doc_comment(&item.attrs);
104                let added_in = get_added_in(&item.attrs);
105                let syn::Expr::Lit(syn::ExprLit {
106                    lit: syn::Lit::Str(lit),
107                    ..
108                }) = &item.expr
109                else {
110                    return None;
111                };
112                let name = lit.value();
113                Some((name, doc, added_in, item.ident.span()))
114            }
115            ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
116                // Extract the environment variable patterns.
117                if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
118                    let doc = get_doc_comment(&item.attrs);
119                    let added_in = get_added_in(&item.attrs);
120                    Some((pattern, doc, added_in, item.sig.span()))
121                } else {
122                    None // Skip if pattern extraction fails.
123                }
124            }
125            _ => None,
126        })
127        .collect();
128
129    // Look for missing attr_added_in and issue a compiler error if any are found.
130    let added_in_errors: Vec<_> = constants
131        .iter()
132        .filter_map(|(name, _, added_in, span)| {
133            added_in.is_none().then_some({
134                let msg = format!(
135                    "missing #[attr_added_in(\"x.y.z\")] on `{name}`\nnote: env vars for an upcoming release should be annotated with `#[attr_added_in(\"next release\")]`"
136                );
137                quote_spanned! {*span => compile_error!(#msg); }
138            })
139        })
140        .collect();
141
142    if !added_in_errors.is_empty() {
143        return quote! { #ast #(#added_in_errors)* }.into();
144    }
145
146    let struct_name = &ast.self_ty;
147    let pairs = constants.iter().map(|(name, doc, added_in, _span)| {
148        if let Some(added_in) = added_in {
149            quote! { (#name, #doc, Some(#added_in)) }
150        } else {
151            quote! { (#name, #doc, None) }
152        }
153    });
154
155    let expanded = quote! {
156        #ast
157
158        impl #struct_name {
159            /// Returns a list of pairs of env var and their documentation defined in this impl block.
160            pub fn metadata<'a>() -> &'a [(&'static str, &'static str, Option<&'static str>)] {
161                &[#(#pairs),*]
162            }
163        }
164    };
165
166    expanded.into()
167}
168
169#[proc_macro_attribute]
170pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
171    item
172}
173
174#[proc_macro_attribute]
175pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
176    item
177}
178
179#[proc_macro_attribute]
180pub fn attr_added_in(_attr: TokenStream, item: TokenStream) -> TokenStream {
181    item
182}