nameth_macro/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use darling::FromMeta;
4use darling::ast::NestedMeta;
5use proc_macro::TokenStream;
6use quote::ToTokens as _;
7use quote::format_ident;
8use quote::quote;
9use syn::Ident;
10
11#[proc_macro_attribute]
12pub fn nameth(attr: TokenStream, tokens: TokenStream) -> TokenStream {
13    let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
14        Ok(args) => args,
15        Err(error) => {
16            return TokenStream::from(darling::Error::from(error).write_errors());
17        }
18    };
19    let attr_args = match NamedMacroArgs::from_list(&attr_args) {
20        Ok(args) => args,
21        Err(error) => {
22            return TokenStream::from(error.write_errors());
23        }
24    };
25
26    let item: syn::Item = match syn::parse(tokens.clone()) {
27        Ok(item) => item,
28        Err(err) => return err.into_compile_error().into(),
29    };
30
31    let crate_name = attr_args.crate_override.as_deref().unwrap_or("nameth");
32    let crate_name = format_ident!("{}", crate_name);
33
34    let name = match item {
35        syn::Item::Struct(item_struct) => process_struct(&crate_name, item_struct),
36        syn::Item::Enum(item_enum) => process_enum(&crate_name, item_enum),
37        syn::Item::Fn(item_fn) => process_fn(item_fn),
38        _ => return quote! { compile_error!("Unexpected item kind"); }.into(),
39    };
40
41    if attr_args.debug {
42        println!("\nGenerated:\n{name}\n");
43    }
44
45    let mut tokens = tokens;
46    tokens.extend(name);
47    return tokens;
48}
49
50fn process_struct(crate_name: &Ident, item_struct: syn::ItemStruct) -> TokenStream {
51    let syn::ItemStruct {
52        ident, generics, ..
53    } = item_struct;
54    let name = ident.to_string();
55    let type_name_const = format_ident!("{}", ident_to_upper_snake_case(&name));
56    let without_defaults = without_defaults(&generics);
57    let param_names_only = param_names_only(&generics);
58    quote! {
59        impl #without_defaults #crate_name::NamedType for #ident #param_names_only {
60            fn type_name() -> &'static str {
61                return #name;
62            }
63        }
64        static #type_name_const: &str = #name;
65    }
66    .into()
67}
68
69fn process_enum(crate_name: &Ident, item_enum: syn::ItemEnum) -> TokenStream {
70    let syn::ItemEnum {
71        ident,
72        generics,
73        variants,
74        ..
75    } = item_enum;
76    let cases: Vec<_> = variants
77        .iter()
78        .map(|variant| {
79            let ident = &variant.ident;
80            let name = ident.to_string();
81            quote! { Self::#ident { .. } => #name, }
82        })
83        .collect();
84
85    let without_defaults = without_defaults(&generics);
86    let param_names_only = param_names_only(&generics);
87    let name = ident.to_string();
88    let type_name_const = format_ident!("{}", ident_to_upper_snake_case(&name));
89    quote! {
90        impl #without_defaults #crate_name::NamedType for #ident #param_names_only {
91            fn type_name() -> &'static str {
92                return #name;
93            }
94        }
95        impl #without_defaults #crate_name::NamedEnumValues for #ident #param_names_only {
96            fn name(&self) -> &'static str {
97                match self {
98                    #(#cases)*
99                }
100            }
101        }
102        static #type_name_const: &str = #name;
103    }
104    .into()
105}
106
107fn process_fn(item_fn: syn::ItemFn) -> TokenStream {
108    let name = item_fn.sig.ident.to_string();
109    let vis = item_fn.vis;
110    let ident = format_ident!("{}", name.to_uppercase());
111    quote! { #vis const #ident : &'static str = #name; }.into()
112}
113
114#[derive(Debug, FromMeta)]
115struct NamedMacroArgs {
116    #[darling(default)]
117    debug: bool,
118
119    #[darling(default)]
120    crate_override: Option<String>,
121}
122
123fn param_names_only(generics: &syn::Generics) -> proc_macro2::TokenStream {
124    let syn::Generics {
125        lt_token: Some(lt_token),
126        params,
127        gt_token: Some(gt_token),
128        where_clause: _,
129    } = generics
130    else {
131        return quote!();
132    };
133    syn::AngleBracketedGenericArguments {
134        colon2_token: None,
135        lt_token: lt_token.to_owned(),
136        args: params
137            .into_iter()
138            .map(|param| match param {
139                syn::GenericParam::Lifetime(x) => {
140                    syn::GenericArgument::Lifetime(x.lifetime.clone())
141                }
142                syn::GenericParam::Type(syn::TypeParam { ident, .. }) => {
143                    syn::GenericArgument::Type(syn::parse2(quote! { #ident }).unwrap())
144                }
145                syn::GenericParam::Const(syn::ConstParam { ident, .. }) => {
146                    syn::GenericArgument::Const(syn::parse2(quote! { #ident }).unwrap())
147                }
148            })
149            .collect(),
150        gt_token: gt_token.to_owned(),
151    }
152    .into_token_stream()
153}
154
155fn without_defaults(generics: &syn::Generics) -> syn::Generics {
156    let mut generics = generics.clone();
157    for param in &mut generics.params {
158        match param {
159            syn::GenericParam::Lifetime(syn::LifetimeParam { attrs, .. }) => {
160                *attrs = vec![];
161            }
162            syn::GenericParam::Type(syn::TypeParam {
163                attrs,
164                eq_token,
165                default,
166                ..
167            }) => {
168                *attrs = vec![];
169                *eq_token = None;
170                *default = None;
171            }
172            syn::GenericParam::Const(syn::ConstParam {
173                attrs,
174                eq_token,
175                default,
176                ..
177            }) => {
178                *attrs = vec![];
179                *eq_token = None;
180                *default = None;
181            }
182        }
183    }
184    generics
185}
186
187fn ident_to_upper_snake_case(name: impl std::fmt::Display + Copy) -> String {
188    let name = name.to_string();
189    let mut result = String::default();
190    let mut last_is_upper = false;
191    for c in name.chars() {
192        if !last_is_upper && c.is_uppercase() {
193            last_is_upper = true;
194            if !result.is_empty() {
195                result.push('_');
196            }
197            result.push(c);
198        } else {
199            last_is_upper = false;
200            result.push_str(&c.to_uppercase().to_string());
201        }
202    }
203    return result;
204}
205
206#[cfg(test)]
207mod tests {
208    #[test]
209    fn ident_to_upper_snake_case() {
210        assert_eq!(
211            "FILE_SYSTEM_IO",
212            super::ident_to_upper_snake_case("FileSystemIO")
213        );
214    }
215}