Skip to main content

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, 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_valid_added_in(added_in: &str) -> bool {
90    added_in == "next release" || is_semantic_version(added_in)
91}
92
93fn is_semantic_version(version: &str) -> bool {
94    let mut components = version.split('.');
95    let Some(major) = components.next() else {
96        return false;
97    };
98    let Some(minor) = components.next() else {
99        return false;
100    };
101    let Some(patch) = components.next() else {
102        return false;
103    };
104
105    if components.next().is_some() {
106        return false;
107    }
108
109    [major, minor, patch].into_iter().all(|component| {
110        !component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
111    })
112}
113
114fn is_hidden(attrs: &[Attribute]) -> bool {
115    attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
116}
117
118/// This attribute is used to generate environment variables metadata for [`uv_static::EnvVars`].
119#[proc_macro_attribute]
120pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
121    let ast = parse_macro_input!(input as ItemImpl);
122
123    let constants: Vec<_> = ast
124        .items
125        .iter()
126        .filter_map(|item| match item {
127            ImplItem::Const(item) if !is_hidden(&item.attrs) => {
128                let doc = get_doc_comment(&item.attrs);
129                let added_in = get_added_in(&item.attrs);
130                let syn::Expr::Lit(syn::ExprLit {
131                    lit: syn::Lit::Str(lit),
132                    ..
133                }) = &item.expr
134                else {
135                    return None;
136                };
137                let name = lit.value();
138                Some((name, doc, added_in, item.ident.span()))
139            }
140            ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
141                // Extract the environment variable patterns.
142                if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
143                    let doc = get_doc_comment(&item.attrs);
144                    let added_in = get_added_in(&item.attrs);
145                    Some((pattern, doc, added_in, item.sig.span()))
146                } else {
147                    None // Skip if pattern extraction fails.
148                }
149            }
150            _ => None,
151        })
152        .collect();
153
154    // Look for missing or invalid attr_added_in values and issue a compiler error if any are found.
155    let added_in_errors: Vec<_> = constants
156        .iter()
157        .filter_map(|(name, _, added_in, span)| {
158            let msg = match added_in {
159                None => format!(
160                    "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\")]`"
161                ),
162                Some(added_in) if !is_valid_added_in(added_in) => format!(
163                    "invalid #[attr_added_in(\"{added_in}\")] on `{name}`\nnote: expected `#[attr_added_in(\"x.y.z\")]` or `#[attr_added_in(\"next release\")]`"
164                ),
165                Some(_) => return None,
166            };
167            Some(quote_spanned! {*span => compile_error!(#msg); })
168        })
169        .collect();
170
171    if !added_in_errors.is_empty() {
172        return quote! { #ast #(#added_in_errors)* }.into();
173    }
174
175    let env_var_names = ast.items.iter().filter_map(|item| {
176        if let ImplItem::Const(item) = item {
177            let syn::Expr::Lit(syn::ExprLit {
178                lit: syn::Lit::Str(lit),
179                ..
180            }) = &item.expr
181            else {
182                return None;
183            };
184            Some(lit.value())
185        } else {
186            None
187        }
188    });
189
190    let struct_name = &ast.self_ty;
191    let pairs = constants.iter().map(|(name, doc, added_in, _span)| {
192        if let Some(added_in) = added_in {
193            quote! { (#name, #doc, Some(#added_in)) }
194        } else {
195            quote! { (#name, #doc, None) }
196        }
197    });
198
199    let expanded = quote! {
200        #ast
201
202        impl #struct_name {
203            /// Returns a list of pairs of env var and their documentation defined in this impl block.
204            pub fn metadata<'a>() -> &'a [(&'static str, &'static str, Option<&'static str>)] {
205                &[#(#pairs),*]
206            }
207
208            /// Returns all environment variable names defined as constants (including hidden ones).
209            pub fn all_names() -> &'static [&'static str] {
210                &[#(#env_var_names),*]
211            }
212        }
213    };
214
215    expanded.into()
216}
217
218#[proc_macro_attribute]
219pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
220    item
221}
222
223#[proc_macro_attribute]
224pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
225    item
226}
227
228#[proc_macro_attribute]
229pub fn attr_added_in(_attr: TokenStream, item: TokenStream) -> TokenStream {
230    item
231}