use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse_macro_input, spanned::Spanned, Data, DataEnum, DataStruct, DataUnion, DeriveInput, Error,
Fields, Ident,
};
#[cfg(feature = "customise")]
use crate::config::derive::get_customisations_from_attrs;
use crate::{
config::derive::DeriveConfig,
util::{crate_module_path, get_docs},
};
#[derive(Copy, Clone, Debug)]
pub enum DocType {
Str,
OptStr,
}
impl ToTokens for DocType {
fn to_tokens(&self, ts: &mut TokenStream2) {
let tokens = match self {
Self::Str => quote! { &'static str },
Self::OptStr => quote! { Option<&'static str> },
};
ts.append_all([tokens]);
}
}
impl DocType {
fn docs_handler_opt(&self) -> Box<dyn Fn(Option<String>, Span) -> syn::Result<TokenStream2>> {
match self {
Self::Str => Box::new(|docs_opt, span| match docs_opt {
Some(docs) => Ok(quote! { #docs }),
None => Err(Error::new(span, "Missing doc comments")),
}),
Self::OptStr => Box::new(|docs_opt, _span| {
let tokens = match docs_opt {
Some(docs) => quote! { Some(#docs) },
None => quote! { None },
};
Ok(tokens)
}),
}
}
fn trait_ident_for(&self, prefix: &str) -> Ident {
let name = match self {
Self::Str => prefix.to_string(),
Self::OptStr => format!("{prefix}Opt"),
};
Ident::new(&name, Span::call_site())
}
}
pub fn documented_impl(input: TokenStream, docs_ty: DocType) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let trait_ident = docs_ty.trait_ident_for("Documented");
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
#[cfg(not(feature = "customise"))]
let config = DeriveConfig::default();
#[cfg(feature = "customise")]
let config = match get_customisations_from_attrs(&input.attrs, "documented") {
Ok(customisations) => DeriveConfig::default().with_customisations(customisations),
Err(err) => return err.into_compile_error().into(),
};
let docs = match get_docs(&input.attrs, config.trim)
.and_then(|docs_opt| docs_ty.docs_handler_opt()(docs_opt, ident.span()))
{
Ok(docs) => docs,
Err(e) => return e.into_compile_error().into(),
};
quote! {
#[automatically_derived]
impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause {
const DOCS: #docs_ty = #docs;
}
}
.into()
}
pub fn documented_fields_impl(input: TokenStream, docs_ty: DocType) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let trait_ident = docs_ty.trait_ident_for("DocumentedFields");
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
#[cfg(not(feature = "customise"))]
let base_config = DeriveConfig::default();
#[cfg(feature = "customise")]
let base_config = match get_customisations_from_attrs(&input.attrs, "documented_fields") {
Ok(customisations) => DeriveConfig::default().with_customisations(customisations),
Err(err) => return err.into_compile_error().into(),
};
let (field_idents, field_docs) = {
let fields_attrs: Vec<_> = match input.data.clone() {
Data::Enum(DataEnum { variants, .. }) => variants
.into_iter()
.map(|v| (v.span(), Some(v.ident), v.attrs))
.collect(),
Data::Struct(DataStruct { fields, .. }) => fields
.into_iter()
.map(|f| (f.span(), f.ident, f.attrs))
.collect(),
Data::Union(DataUnion { fields, .. }) => fields
.named
.into_iter()
.map(|f| (f.span(), f.ident, f.attrs))
.collect(),
};
match fields_attrs
.into_iter()
.map(|(span, ident, attrs)| {
#[cfg(not(feature = "customise"))]
let config = base_config;
#[cfg(feature = "customise")]
let config = base_config.with_customisations(get_customisations_from_attrs(
&attrs,
"documented_fields",
)?);
get_docs(&attrs, config.trim)
.and_then(|docs_opt| docs_ty.docs_handler_opt()(docs_opt, span))
.map(|docs| (ident, docs))
})
.collect::<syn::Result<Vec<_>>>()
{
Ok(t) => t.into_iter().unzip::<_, _, Vec<_>, Vec<_>>(),
Err(e) => return e.into_compile_error().into(),
}
};
let phf_match_arms = field_idents
.into_iter()
.enumerate()
.filter_map(|(i, o)| o.map(|ident| (i, ident.to_string())))
.map(|(i, ident)| quote! { #ident => #i, })
.collect::<Vec<_>>();
let documented_module_path = crate_module_path();
quote! {
#[automatically_derived]
impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause {
const FIELD_DOCS: &'static [#docs_ty] = &[#(#field_docs),*];
fn __documented_get_index<__Documented_T: AsRef<str>>(field_name: __Documented_T) -> Option<usize> {
use #documented_module_path::_private_phf_reexport_for_macro as phf;
static PHF: phf::Map<&'static str, usize> = phf::phf_map! {
#(#phf_match_arms)*
};
PHF.get(field_name.as_ref()).copied()
}
}
}
.into()
}
pub fn documented_variants_impl(input: TokenStream, docs_ty: DocType) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let trait_ident = docs_ty.trait_ident_for("DocumentedVariants");
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
#[cfg(not(feature = "customise"))]
let base_config = DeriveConfig::default();
#[cfg(feature = "customise")]
let base_config = match get_customisations_from_attrs(&input.attrs, "documented_variants") {
Ok(customisations) => DeriveConfig::default().with_customisations(customisations),
Err(err) => return err.into_compile_error().into(),
};
let variants_docs = {
let Data::Enum(DataEnum { variants, .. }) = input.data else {
return Error::new(
input.span(), "DocumentedVariants can only be used on enums.\n\
For structs and unions, use DocumentedFields instead.",
)
.into_compile_error()
.into();
};
match variants
.into_iter()
.map(|v| (v.span(), v.ident, v.fields, v.attrs))
.map(|(span, ident, field, attrs)| {
#[cfg(not(feature = "customise"))]
let config = base_config;
#[cfg(feature = "customise")]
let config = base_config.with_customisations(get_customisations_from_attrs(
&attrs,
"documented_variants",
)?);
get_docs(&attrs, config.trim)
.and_then(|docs_opt| docs_ty.docs_handler_opt()(docs_opt, span))
.map(|docs| (ident, field, docs))
})
.collect::<syn::Result<Vec<_>>>()
{
Ok(t) => t,
Err(e) => return e.into_compile_error().into(),
}
};
let match_arms = variants_docs
.into_iter()
.map(|(ident, fields, docs)| {
let pat = match fields {
Fields::Unit => quote! { Self::#ident },
Fields::Unnamed(_) => quote! { Self::#ident(..) },
Fields::Named(_) => quote! { Self::#ident{..} },
};
quote! { #pat => #docs, }
})
.collect::<Vec<_>>();
quote! {
#[automatically_derived]
impl #impl_generics documented::#trait_ident for #ident #ty_generics #where_clause {
fn get_variant_docs(&self) -> #docs_ty {
match self {
#(#match_arms)*
}
}
}
}
.into()
}