enum-repr-conv 0.1.0

enum_repr_conv is a proc-macro library that provides a convenient attribute-style `repr_conv()` macro to specify a primitive integer type used for representing variants of the annotated fieldless enum as values of this type.
Documentation
use proc_macro2::TokenStream;
use syn::{Error, Ident, Item, ItemEnum, Meta};

pub(crate) struct Parsed {
    pub attr: Ident,
    pub item: ItemEnum,
}

fn format_error_output(msg: &str, help: &str) -> String {
    // Shamelessly copied from
    // https://gitlab.com/CreepySkeleton/proc-macro-error/-/blob/a43601004db560a7e02ee5d45f26eda757dec259/src/diagnostic.rs#L240
    format!("{msg}\n\n  = help: {help}\n\n")
}

pub(crate) fn parse(attr: TokenStream, input: TokenStream) -> syn::Result<Parsed> {
    const HELP_ATTR: &str = "`repr_conv` expects a single attribute which is an integer type";
    const HELP_ITEM: &str = "`repr_conv` can only be used on fieldless enums";
    const INTEGRAL_TYPES_SORTED: [&str; 12] = [
        "i128", "i16", "i32", "i64", "i8", "isize", "u128", "u16", "u32", "u64", "u8", "usize",
    ];
    let attr = match syn::parse2::<Meta>(attr) {
        Ok(Meta::Path(attr)) => {
            attr.require_ident()?;
            let attr = attr.get_ident().unwrap().clone();
            match INTEGRAL_TYPES_SORTED.binary_search(&attr.to_string().as_str()) {
                Ok(_) => Ok(attr),
                Err(_) => Err(Error::new_spanned(
                    &attr,
                    format_error_output("unsupported representation type", HELP_ATTR),
                )),
            }
        }
        Ok(attr) => Err(Error::new_spanned(
            &attr,
            format_error_output("invalid attribute format", HELP_ATTR),
        )),
        Err(err) => Err(Error::new(
            err.span(),
            format_error_output(&err.to_string(), HELP_ATTR),
        )),
    }?;

    let item = match syn::parse2::<Item>(input.clone()) {
        Ok(Item::Enum(item)) => Ok(item),
        Ok(_) => Err(Error::new_spanned(
            &input,
            format_error_output("item is not an enum", HELP_ITEM),
        )),
        Err(_) => unreachable!(), // attribute macros can only be attached to Items
    }?;

    for variant in item.variants.iter() {
        if !variant.fields.is_empty() {
            return Err(Error::new_spanned(
                &item,
                format_error_output("must be a fieldless enum", HELP_ITEM),
            ));
        }
    }

    Ok(Parsed { attr, item })
}

#[cfg(test)]
mod tests {
    use quote::quote;

    use super::*;

    #[test]
    fn valid_syntax_fieldless_enum() {
        assert!(parse(
            quote!(u32),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_ok());
    }

    #[test]
    fn invalid_syntax_empty_attr() {
        assert!(parse(
            quote!(),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_non_ident_attr() {
        assert!(parse(
            quote!(Option<u16>),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_list_attr() {
        assert!(parse(
            quote!(u16, u32),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_name_value_attr() {
        assert!(parse(
            quote!(type = u16),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_malformed_attr() {
        assert!(parse(
            quote!(!@#$%),
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_unsupported_attr() {
        assert!(parse(
            quote!(C), // valid as in #[repr(C)] but not for this macro
            quote!(
                enum Fieldless {
                    Zero = 0,
                    One,
                    Two = 0x02,
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_non_enum_item() {
        assert!(parse(
            quote!(u32),
            quote!(
                struct NotAnEnum {
                    zero: u32,
                    one: String,
                    two: (),
                }
            ),
        )
        .is_err());
    }

    #[test]
    fn invalid_syntax_enum_with_fields() {
        assert!(parse(
            quote!(u32),
            quote!(
                enum WithFields {
                    Zero,
                    One(u16),
                    Two { s: String },
                }
            ),
        )
        .is_err());
    }
}