Skip to content

Commit

Permalink
Split all derive macros into non-opt & opt variants
Browse files Browse the repository at this point in the history
  • Loading branch information
cyqsimon committed Oct 16, 2024
1 parent 4f9b45d commit 9e158a5
Show file tree
Hide file tree
Showing 10 changed files with 527 additions and 278 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use documented::{Documented, DocumentedFields, DocumentedVariants};
/// Trying is the first step to failure.
#[derive(Documented, DocumentedFields, DocumentedVariants)]
enum AlwaysPlay {
/// And Kb8.
#[allow(dead_code)]
Kb1,
/// But only if you are white.
Expand All @@ -25,16 +26,13 @@ assert_eq!(AlwaysPlay::DOCS, "Trying is the first step to failure.");
// DocumentedFields
assert_eq!(
AlwaysPlay::FIELD_DOCS,
[None, Some("But only if you are white.")]
);
assert_eq!(
AlwaysPlay::get_field_docs("F6"),
Ok("But only if you are white.")
["And Kb8.", "But only if you are white."]
);
assert_eq!(AlwaysPlay::get_field_docs("Kb1"), Ok("And Kb8."));

// DocumentedVariants
assert_eq!(
AlwaysPlay::F6.get_variant_docs(),
Ok("But only if you are white.")
"But only if you are white."
);
```
262 changes: 262 additions & 0 deletions documented-macros/src/derive_impl.rs
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()
}
Loading

0 comments on commit 9e158a5

Please sign in to comment.