Generate environment variables doc from code (#8493)

## Summary

Resolves #8417

I've just begun learning procedural macros, so this PR is more of a
proof of concept. It's still a work in progress, and I welcome any
assistance or feedback.
This commit is contained in:
Jo 2024-11-03 22:31:38 +08:00 committed by GitHub
parent 545a55f58f
commit 3dfedf1fef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 491 additions and 135 deletions

View file

@ -2,7 +2,7 @@ mod options_metadata;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl, LitStr};
#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
@ -49,3 +49,92 @@ fn impl_combine(ast: &DeriveInput) -> TokenStream {
};
gen.into()
}
fn get_doc_comment(attrs: &[Attribute]) -> String {
attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr) = &meta.value {
if let syn::Lit::Str(str) = &expr.lit {
return Some(str.value().trim().to_string());
}
}
}
}
None
})
.collect::<Vec<_>>()
.join("\n")
}
fn get_env_var_pattern_from_attr(attrs: &[Attribute]) -> Option<String> {
attrs
.iter()
.find(|attr| attr.path().is_ident("attr_env_var_pattern"))
.and_then(|attr| attr.parse_args::<LitStr>().ok())
.map(|lit_str| lit_str.value())
}
fn is_hidden(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
}
/// This attribute is used to generate environment variables metadata for [`uv_static::EnvVars`].
#[proc_macro_attribute]
pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemImpl);
let constants: Vec<_> = ast
.items
.iter()
.filter_map(|item| match item {
ImplItem::Const(item) if !is_hidden(&item.attrs) => {
let name = item.ident.to_string();
let doc = get_doc_comment(&item.attrs);
Some((name, doc))
}
ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
// Extract the environment variable patterns.
if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
let doc = get_doc_comment(&item.attrs);
Some((pattern, doc))
} else {
None // Skip if pattern extraction fails.
}
}
_ => None,
})
.collect();
let struct_name = &ast.self_ty;
let pairs = constants.iter().map(|(name, doc)| {
quote! {
(#name, #doc)
}
});
let expanded = quote! {
#ast
impl #struct_name {
/// Returns a list of pairs of env var and their documentation defined in this impl block.
pub fn metadata<'a>() -> &'a [(&'static str, &'static str)] {
&[#(#pairs),*]
}
}
};
expanded.into()
}
#[proc_macro_attribute]
pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}