efbuilder 0.0.4

A procedural macro for generating efficient builders.
Documentation
use {
    proc_macro2::Ident,
    proc_macro::TokenStream,
    proc_macro_error::{abort, abort_call_site, proc_macro_error, ResultExt},
    quote::quote,
    std::iter::repeat,
    syn::{
        Attribute,
        Data,
        DataStruct,
        DeriveInput,
        Field,
        LifetimeDef,
        parse_macro_input,
        spanned::Spanned,
        Type,
        TypeParam,
    },
};

#[proc_macro_derive(Builder)]
#[proc_macro_error]
pub fn derive_builder(input: TokenStream) -> TokenStream
{
    let ast = parse_macro_input!(input as DeriveInput);
    let DeriveInput {
        ident,
        generics,
        data,
        vis,
        ..
    } = ast;

    let fields = match data {
        Data::Struct(DataStruct { fields, .. }) => fields,
        _ => abort_call_site!("#[derive(Builder)] is only supported for structs")
    };

    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let mut lifetimes = generics.lifetimes()
        .map(|LifetimeDef { lifetime, .. }| lifetime)
        .peekable();
    let lifetimes = if lifetimes.peek().is_some() {
        quote! { #(#lifetimes),*, }
    } else {
        quote! {}
    };

    let mut impl_lifetimes = generics.lifetimes()
        .map(
            |LifetimeDef { lifetime, bounds, .. }| {
                if bounds.is_empty() {
                    quote! { #lifetime }
                } else {
                    quote! { #lifetime: #bounds }
                }
            }
        )
        .peekable();
    let impl_lifetimes = if impl_lifetimes.peek().is_some() {
        quote! { #(#impl_lifetimes),*, }
    } else {
        quote! {}
    };

    let mut type_params = generics.type_params().peekable();
    let type_params = if type_params.peek().is_some() {
        quote! { #(#type_params),*, }
    } else {
        quote! {}
    };

    struct FieldInfo {
        doc_attrs: Vec<Attribute>,
        ident: Ident,
        ty: Type,
    }

    let fields_to_init = fields.into_iter()
        .map(
            |field| {
                let field_span = field.span();
                let Field { attrs, ident, ty, .. } = field;
                let ident = ident.unwrap_or_else(
                    || abort!(
                        field_span,
                        "#[derive(Builder)]: only named fields are currently supported"
                    )
                );
                let doc_attrs = attrs.iter()
                    .filter(
                        |v| {
                            v.parse_meta()
                                .map(|meta| meta.path().is_ident("doc"))
                                .unwrap_or(false)
                        }
                    )
                    .cloned()
                    .collect();
                FieldInfo {
                    doc_attrs,
                    ident,
                    ty,
                }
            }
        )
        .collect::<Vec<_>>();

    let builder_name = format!("{ident}Builder");
    let builder_name = syn::parse_str::<Ident>(&builder_name)
        .map_err(|e| syn::Error::new(ident.span(), e))
        .expect_or_abort("#[derive(Builder)]: corresponding builder name is not an ident");

    fn generate_init_indicator(ident: &Ident) -> Ident
    {
        let const_generic_parameter = format!("{ident}_INIT");
        syn::parse_str::<Ident>(&const_generic_parameter)
            .map_err(|e| syn::Error::new(ident.span(), e))
            .expect_or_abort(
                "#[derive(Builder)]: can't generate corresponding const-generic indicator"
            )
    }

    let mut builder_consts = fields_to_init.iter()
        .map(|FieldInfo { ident, .. }| ident)
        .map(generate_init_indicator)
        .map(|ind| quote! { const #ind: bool })
        .peekable();
    let builder_consts = if builder_consts.peek().is_some() {
        quote! { #(#builder_consts),*, }
    } else {
        quote! {}
    };
    let builder_body = fields_to_init.iter()
        .map(
            |FieldInfo { ident, ty, .. }| quote! {
                #ident: ::std::mem::MaybeUninit<#ty>
            }
        );

    let builder_body = quote! {
        #vis struct #builder_name <#impl_lifetimes #builder_consts #type_params>
            #where_clause
        {
            #(#builder_body),*
        }
    };


    let mut all_consts_true = repeat(quote! { true })
        .take(fields_to_init.len())
        .peekable();
    let all_consts_true = if all_consts_true.peek().is_some() {
        quote! { #(#all_consts_true),*, }
    } else {
        quote! {}
    };
    let type_idents = generics.type_params()
        .map(|TypeParam { ident, .. }| ident)
        .collect::<Vec<_>>();
    let fields = fields_to_init.iter()
        .map(|FieldInfo { ident, .. }| ident);
    let fields_assume_init = fields_to_init.iter()
        .map(|FieldInfo { ident, .. }| quote! { #ident: #ident.assume_init() });

    let build_impl = quote! {
        impl #impl_generics #builder_name <#lifetimes #all_consts_true #(#type_idents),*>
            #where_clause
        {
            #[inline(always)]
            #[doc = concat!(" Builds [`", ::std::stringify!(#ident), "`].")]
            pub const fn build(self) -> #ident #ty_generics
            {
                let Self {
                    #(#fields),*
                } = self;
                unsafe {
                    #ident {
                        #(#fields_assume_init),*
                    }
                }
            }
        }
    };


    let mut all_consts_false = repeat(quote! { false })
        .take(fields_to_init.len())
        .peekable();
    let all_consts_false = if all_consts_false.peek().is_some() {
        quote! { #(#all_consts_false),*, }
    } else {
        quote! {}
    };
    let fields_uninit = fields_to_init.iter()
        .map(|FieldInfo { ident, .. }| quote! { #ident: ::std::mem::MaybeUninit::uninit() });

    let new_impl = quote! {
        impl #impl_generics #builder_name <#lifetimes #all_consts_false #(#type_idents),*>
            #where_clause
        {
            #[must_use]
            #[inline(always)]
            #[doc = concat!(
                " Creates a new instance of [`",
                ::std::stringify!(#builder_name),
                "`]."
            )]
            pub const fn new() -> Self
            {
                Self {
                    #(#fields_uninit),*
                }
            }
        }
    };

    let methods = fields_to_init.iter()
        .enumerate()
        .map(
            |(cur_idx, FieldInfo { doc_attrs, ident, ty })| {
                let mut impl_consts = fields_to_init.iter()
                    .map(|FieldInfo { ident, .. }| ident)
                    .enumerate()
                    .filter_map(
                        |(idx, field)| if idx != cur_idx {
                            Some(field)
                        } else {
                            None
                        }
                    )
                    .map(generate_init_indicator)
                    .map(|ind| quote! { const #ind: bool })
                    .peekable();
                let impl_consts = if impl_consts.peek().is_some() {
                    quote! { #(#impl_consts),*, }
                } else {
                    quote! {}
                };

                let mut consts_self = fields_to_init.iter()
                    .map(|FieldInfo { ident, .. }| ident)
                    .enumerate()
                    .map(
                        |(idx, field)| if idx != cur_idx {
                            let ind = generate_init_indicator(field);
                            quote! { #ind }
                        } else {
                            quote! { false }
                        }
                    )
                    .peekable();
                let consts_self = if consts_self.peek().is_some() {
                    quote! { #(#consts_self),*, }
                } else {
                    quote! {}
                };

                let mut consts_res = fields_to_init.iter()
                    .map(|FieldInfo { ident, .. }| ident)
                    .enumerate()
                    .map(
                        |(idx, field)| if idx != cur_idx {
                            let ind = generate_init_indicator(field);
                            quote! { #ind }
                        } else {
                            quote! { true }
                        }
                    )
                    .peekable();
                let consts_res = if consts_res.peek().is_some() {
                    quote! { #(#consts_res),*, }
                } else {
                    quote! {}
                };

                let mut fields = fields_to_init.iter()
                    .map(|FieldInfo { ident, .. }| ident)
                    .enumerate()
                    .filter_map(
                        |(idx, value)| if idx != cur_idx {
                            Some(value)
                        } else {
                            None
                        }
                    )
                    .peekable();
                let fields = if fields.peek().is_some() {
                    quote! { #(#fields),*, }
                } else {
                    quote! {}
                };

                quote! {
                    impl<#impl_lifetimes #impl_consts #type_params>
                    #builder_name <#lifetimes #consts_self #(#type_idents),*>
                        #where_clause
                    {
                        #[must_use]
                        #(#doc_attrs)*
                        #[inline(always)]
                        pub fn #ident(
                            self,
                            value: #ty) -> #builder_name <#lifetimes #consts_res #(#type_idents),*>
                        {
                            let Self {
                                #fields
                                ..
                            } = self;
                            #builder_name {
                                #fields
                                #ident: ::std::mem::MaybeUninit::new(value),
                            }
                        }
                    }
                }
            }
        );

    let tokens = quote! {
        #builder_body
        #build_impl
        #new_impl
        #(#methods)*
    };
    tokens.into()
}