read-only-derive 0.1.1

Proc-macro support crate for read-only
Documentation
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::visit_mut::{self, VisitMut};
use syn::{
    Data, DeriveInput, Error, Field, Fields, Ident, Path, Result, Token, Visibility, parse_quote,
};

type Punctuated = syn::punctuated::Punctuated<Field, Token![,]>;

pub fn embed(input: DeriveInput) -> Result<TokenStream> {
    let call_site = Span::call_site();
    let mut input = input;

    let ident = input.ident.clone();
    let vis = input.vis.clone();
    let generics = input.generics.clone();
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
    let self_path: Path = parse_quote!(#ident #ty_generics);
    let orig_attrs = input.attrs.clone();
    let mut attr_errors = Vec::new();

    let readonly_indices;
    let all_fields;
    let mut doc_fields;

    match &mut input.data {
        Data::Struct(data) => match &mut data.fields {
            Fields::Named(fields) => {
                if fields.named.is_empty() {
                    return Err(Error::new(call_site, "input must have at least one field"));
                }

                let stripped_readonly_indices =
                    find_and_strip_readonly_attrs(&mut fields.named, &mut attr_errors);

                if stripped_readonly_indices.is_empty() {
                    return Err(Error::new(
                        call_site,
                        "expected at least one #[readonly] field",
                    ));
                }

                let conflicting_span = conflicting_read_only_span(&fields.named);

                if conflicting_span.is_some() {
                    return Err(Error::new(
                        conflicting_span.unwrap_or(call_site),
                        "field name `read_only` conflicts with generated field",
                    ));
                }

                let extracted_fields: Vec<Field> =
                    std::mem::take(&mut fields.named).into_iter().collect();
                readonly_indices = stripped_readonly_indices;
                doc_fields = extracted_fields.clone();
                all_fields = extracted_fields;
            }
            Fields::Unnamed(_) | Fields::Unit => {
                return Err(Error::new(call_site, "input must have named fields"));
            }
        },
        Data::Enum(_) | Data::Union(_) => {
            return Err(Error::new(call_site, "input must be a struct"));
        }
    }

    let mut readonly_fields = Vec::new();
    let mut remaining_fields = Vec::new();

    for (i, field) in doc_fields.iter_mut().enumerate() {
        if readonly_indices.contains(&i) && matches!(field.vis, Visibility::Inherited) {
            field.vis = vis.clone();
        }
    }

    for (i, mut field) in all_fields.into_iter().enumerate() {
        if readonly_indices.contains(&i) {
            if let Visibility::Inherited = field.vis {
                field.vis = vis.clone();
            }
            ReplaceSelf::new(&self_path).visit_type_mut(&mut field.ty);
            field.attrs.clear();
            readonly_fields.push(field);
        } else {
            remaining_fields.push(field);
        }
    }

    let readonly_ident = Ident::new(&format!("ReadOnly{}", ident), call_site);

    let read_only_field: Field = parse_quote! {
        read_only: #readonly_ident #ty_generics
    };

    let attr_errors_tokens = attr_errors.iter().map(Error::to_compile_error);
    let outer_struct = if cfg!(feature = "docs") {
        quote! {
            #[cfg(doc)]
            #(#orig_attrs)*
            #vis struct #ident #impl_generics #where_clause {
                #(#doc_fields),*
            }

            #[cfg(not(doc))]
            #(#orig_attrs)*
            #vis struct #ident #impl_generics #where_clause {
                #read_only_field,
                #(#remaining_fields),*
            }
        }
    } else {
        quote! {
            #(#orig_attrs)*
            #vis struct #ident #impl_generics #where_clause {
                #read_only_field,
                #(#remaining_fields),*
            }
        }
    };
    let deref_impl = if cfg!(feature = "docs") {
        quote! {
            #[cfg(not(doc))]
            const _: () = {
                impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause {
                    type Target = #readonly_ident #ty_generics;

                    fn deref(&self) -> &Self::Target {
                        &self.read_only
                    }
                }
            };
        }
    } else {
        quote! {
            const _: () = {
                impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause {
                    type Target = #readonly_ident #ty_generics;

                    fn deref(&self) -> &Self::Target {
                        &self.read_only
                    }
                }
            };
        }
    };

    Ok(quote! {
        #[doc(hidden)]
        #vis struct #readonly_ident #impl_generics #where_clause {
            #(#readonly_fields),*
        }

        #outer_struct

        #deref_impl

        #(#attr_errors_tokens)*
    })
}

fn find_and_strip_readonly_attrs(fields: &mut Punctuated, errors: &mut Vec<Error>) -> Vec<usize> {
    let mut indices = Vec::new();

    for (i, field) in fields.iter_mut().enumerate() {
        let mut readonly_attr_index = None;

        for (j, attr) in field.attrs.iter().enumerate() {
            if attr.path().is_ident("readonly") {
                if let Err(err) = attr.meta.require_path_only() {
                    errors.push(err);
                }
                readonly_attr_index = Some(j);
                break;
            }
        }

        if let Some(readonly_attr_index) = readonly_attr_index {
            field.attrs.remove(readonly_attr_index);
            indices.push(i);
        }
    }

    indices
}

fn conflicting_read_only_span(fields: &Punctuated) -> Option<Span> {
    fields.iter().find_map(|field| {
        field
            .ident
            .as_ref()
            .filter(|id| *id == "read_only")
            .map(Ident::span)
    })
}

struct ReplaceSelf<'a> {
    with: &'a Path,
}

impl<'a> ReplaceSelf<'a> {
    fn new(with: &'a Path) -> Self {
        ReplaceSelf { with }
    }
}

impl<'a> VisitMut for ReplaceSelf<'a> {
    fn visit_path_mut(&mut self, path: &mut Path) {
        if path.is_ident("Self") {
            let span = path.segments[0].ident.span();
            *path = self.with.clone();
            path.segments[0].ident.set_span(span);
        } else {
            visit_mut::visit_path_mut(self, path);
        }
    }
}