gix-macros 0.2.1

Proc-macro utilities for gix
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, ImplItem, Item, ItemStruct, LitStr, Visibility, parse::Parse, parse_quote, spanned::Spanned};

struct Instability {
    since: LitStr,
    reason: Option<LitStr>,
}

impl Parse for Instability {
    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
        let mut since = None;
        let mut reason = None;
        while !input.is_empty() {
            let name: syn::Ident = input.parse()?;
            let field = match name.to_string().as_str() {
                "since" => &mut since,
                "reason" => &mut reason,
                _ => return Err(syn::Error::new(name.span(), "expected `since` or `reason`")),
            };
            if field.is_some() {
                return Err(syn::Error::new(name.span(), format!("duplicate `{name}`")));
            }
            input.parse::<syn::Token![=]>()?;
            let value: LitStr = input.parse()?;
            if name == "since" && !valid_date(&value.value()) {
                return Err(syn::Error::new(
                    value.span(),
                    "expected a valid date in YYYY-MM-DD format",
                ));
            }
            if name == "reason" && value.value().trim().is_empty() {
                return Err(syn::Error::new(
                    value.span(),
                    "the instability reason must not be empty",
                ));
            }
            *field = Some(value);
            if !input.is_empty() {
                input.parse::<syn::Token![,]>()?;
            }
        }
        let since = since.ok_or_else(|| input.error("missing required `since = \"YYYY-MM-DD\"`"))?;
        Ok(Self { since, reason })
    }
}

fn valid_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    if bytes.len() != 10
        || bytes[4] != b'-'
        || bytes[7] != b'-'
        || bytes
            .iter()
            .enumerate()
            .any(|(index, byte)| !matches!(index, 4 | 7) && !byte.is_ascii_digit())
    {
        return false;
    }
    let number = |digits: &[u8]| {
        digits
            .iter()
            .fold(0u16, |value, digit| value * 10 + u16::from(digit - b'0'))
    };
    let year = number(&bytes[..4]);
    let month = number(&bytes[5..7]);
    let day = number(&bytes[8..]);
    let days = match month {
        4 | 6 | 9 | 11 => 30,
        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
        2 => 28,
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        _ => return false,
    };
    year != 0 && (1..=days).contains(&day)
}

impl Instability {
    fn document(&self, attrs: &mut Vec<Attribute>) {
        let mut note = format!("\n\n**Unstable:** Since {}.", self.since.value());
        if let Some(reason) = &self.reason {
            note.push(' ');
            note.push_str(&reason.value());
        }
        attrs.push(parse_quote!(#[doc = #note]));
    }
}

fn restrict(vis: &mut Visibility) -> syn::Result<()> {
    if !matches!(vis, Visibility::Public(_)) {
        return Err(syn::Error::new(vis.span(), "expected an unrestricted `pub` item"));
    }
    *vis = parse_quote!(pub(crate));
    Ok(())
}

pub(crate) fn item(attrs: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
    let reason: Instability = syn::parse2(attrs)?;
    let mut stable: Item = syn::parse2(input)?;
    if let Item::Impl(item) = &mut stable {
        if item.trait_.is_some() {
            reason.document(&mut item.attrs);
            return Ok(quote!(#[cfg(feature = "unstable")] #stable));
        }
        let mut unstable = item.clone();
        for (stable, unstable) in item.items.iter_mut().zip(&mut unstable.items) {
            let (vis, attrs) = match (stable, unstable) {
                (ImplItem::Fn(stable), ImplItem::Fn(unstable)) => (&mut stable.vis, &mut unstable.attrs),
                (ImplItem::Const(stable), ImplItem::Const(unstable)) => (&mut stable.vis, &mut unstable.attrs),
                (ImplItem::Type(stable), ImplItem::Type(unstable)) => (&mut stable.vis, &mut unstable.attrs),
                (ImplItem::Macro(item), _) => {
                    return Err(syn::Error::new_spanned(
                        item,
                        "cannot restrict the visibility of macro-generated impl members",
                    ));
                }
                _ => continue,
            };
            if matches!(vis, Visibility::Public(_)) {
                restrict(vis)?;
                reason.document(attrs);
            }
        }
        return Ok(quote! {
            #[cfg(feature = "unstable")]
            #unstable
            #[cfg(not(feature = "unstable"))]
            #[allow(dead_code)]
            #stable
        });
    }
    let mut unstable = stable.clone();
    let (vis, attrs) = match (&mut stable, &mut unstable) {
        (Item::Const(stable), Item::Const(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Enum(stable), Item::Enum(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::ExternCrate(stable), Item::ExternCrate(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Fn(stable), Item::Fn(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Mod(stable), Item::Mod(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Static(stable), Item::Static(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Struct(stable), Item::Struct(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Trait(stable), Item::Trait(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::TraitAlias(stable), Item::TraitAlias(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Type(stable), Item::Type(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Union(stable), Item::Union(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        (Item::Use(stable), Item::Use(unstable)) => (&mut stable.vis, &mut unstable.attrs),
        _ => {
            return Err(syn::Error::new(
                stable.span(),
                "expected a public item or implementation",
            ));
        }
    };
    restrict(vis)?;
    reason.document(attrs);
    Ok(quote! {
        #[cfg(feature = "unstable")]
        #unstable
        #[cfg(not(feature = "unstable"))]
        #[allow(dead_code, unused_imports)]
        #stable
    })
}

pub(crate) fn fields(attrs: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
    if !attrs.is_empty() {
        return Err(syn::Error::new_spanned(attrs, "put `#[unstable]` on each gated field"));
    }
    let mut stable: ItemStruct = syn::parse2(input)?;
    let mut unstable = stable.clone();
    let mut annotated = false;
    for (stable, unstable) in stable.fields.iter_mut().zip(&mut unstable.fields) {
        let mut reason = None;
        for attr in &stable.attrs {
            if attr.path().is_ident("unstable") {
                if reason.is_some() {
                    return Err(syn::Error::new_spanned(
                        attr,
                        "the field already has an instability annotation",
                    ));
                }
                reason = Some(if matches!(&attr.meta, syn::Meta::Path(_)) {
                    return Err(syn::Error::new_spanned(
                        attr,
                        "missing required `since = \"YYYY-MM-DD\"`",
                    ));
                } else {
                    attr.parse_args::<Instability>()?
                });
            }
        }
        if let Some(reason) = reason {
            annotated = true;
            restrict(&mut stable.vis)?;
            stable.attrs.retain(|attr| !attr.path().is_ident("unstable"));
            unstable.attrs.retain(|attr| !attr.path().is_ident("unstable"));
            reason.document(&mut unstable.attrs);
        }
    }
    if !annotated {
        return Err(syn::Error::new_spanned(
            stable,
            "expected at least one field with `#[unstable]`",
        ));
    }
    Ok(quote! {
        #[cfg(feature = "unstable")]
        #unstable
        #[cfg(not(feature = "unstable"))]
        #[allow(dead_code)]
        #stable
    })
}