-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split all derive macros into non-opt & opt variants
- Loading branch information
Showing
10 changed files
with
527 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
//! Shared implementation for non-opt and opt variants of the derive macros. | ||
//! | ||
//! All functions in this module use the dependency injection pattern to | ||
//! generate the correct trait implementation for both macro variants. | ||
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, | ||
}; | ||
|
||
use crate::{ | ||
config::derive::{get_customisations_from_attrs, DeriveConfig}, | ||
util::{crate_module_path, get_docs}, | ||
}; | ||
|
||
/// The type of the doc comment. | ||
#[derive(Copy, Clone, Debug)] | ||
pub enum DocType { | ||
/// &'static str | ||
Str, | ||
/// Option<&'static 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 { | ||
/// Get the closure that determines how to handle optional docs. | ||
/// The closure takes two arguments: | ||
/// | ||
/// 1. The optional doc comments on an item | ||
/// 2. The span on which to report any errors | ||
/// | ||
/// And fallibly returns the tokenised doc comments. | ||
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| { | ||
// quote macro needs some help with `Option`s | ||
// see: https://github.com/dtolnay/quote/issues/213 | ||
let tokens = match docs_opt { | ||
Some(docs) => quote! { Some(#docs) }, | ||
None => quote! { None }, | ||
}; | ||
Ok(tokens) | ||
}), | ||
} | ||
} | ||
|
||
/// Get the trait identifier, given a prefix. | ||
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()) | ||
} | ||
} | ||
|
||
/// Shared implementation of `Documented` & `DocumentedOpt`. | ||
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() | ||
} | ||
|
||
/// Shared implementation of `DocumentedFields` & `DocumentedFieldsOpt`. | ||
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(); | ||
|
||
// `#[documented_fields(...)]` on container type | ||
#[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() | ||
} | ||
|
||
/// Shared implementation of `DocumentedVariants` & `DocumentedVariantsOpt`. | ||
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(); | ||
|
||
// `#[documented_variants(...)]` on container type | ||
#[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(), // this targets the `struct`/`union` keyword | ||
"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<_>>(); | ||
|
||
// IDEA: I'd like to use phf here, but it doesn't seem to be possible at the moment, | ||
// because there isn't a way to get an enum's discriminant at compile time | ||
// if this becomes possible in the future, or alternatively you have a good workaround, | ||
// improvement suggestions are more than welcomed | ||
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() | ||
} |
Oops, something went wrong.