odra-ir 0.7.1

Data structures for Odra IR (intermediate representation).
Documentation
use std::{collections::HashSet, convert::TryFrom};

use itertools::{Either, Itertools};
use proc_macro2::{Ident, Span};
use quote::ToTokens;
use syn::{Lit, Path};

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Attribute {
    Odra(OdraAttribute),
    Other(syn::Attribute)
}

impl TryFrom<syn::Attribute> for Attribute {
    type Error = syn::Error;

    fn try_from(attr: syn::Attribute) -> Result<Self, Self::Error> {
        if attr.path.is_ident("odra") {
            return <OdraAttribute as TryFrom<_>>::try_from(attr).map(Into::into);
        }
        Ok(Attribute::Other(attr))
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OdraAttribute {
    kinds: Vec<AttrKind>
}

impl OdraAttribute {
    pub fn is_constructor(&self) -> bool {
        self.kinds
            .iter()
            .any(|attr_kind| matches!(attr_kind, &AttrKind::Constructor))
    }

    pub fn is_payable(&self) -> bool {
        self.kinds
            .iter()
            .any(|attr_kind| matches!(attr_kind, &AttrKind::Payable))
    }

    pub fn is_non_reentrant(&self) -> bool {
        self.kinds
            .iter()
            .any(|attr_kind| matches!(attr_kind, &AttrKind::NonReentrant))
    }

    pub fn using(&self) -> Vec<String> {
        self.kinds
            .iter()
            .filter_map(|attr| match attr {
                AttrKind::Using(fields) => Some(fields.clone()),
                _ => None
            })
            .flatten()
            .dedup()
            .collect::<Vec<_>>()
    }
}

impl From<OdraAttribute> for Attribute {
    fn from(odra_attribute: OdraAttribute) -> Self {
        Attribute::Odra(odra_attribute)
    }
}

impl TryFrom<syn::Attribute> for OdraAttribute {
    type Error = syn::Error;

    fn try_from(attrs: syn::Attribute) -> Result<Self, Self::Error> {
        let kinds = attrs
            .parse_meta()
            .map(|meta| match meta {
                syn::Meta::List(meta_list) => {
                    let attr_kinds = meta_list
                        .nested
                        .into_iter()
                        .map(<AttrKind as TryFrom<_>>::try_from)
                        .collect::<Result<Vec<_>, syn::Error>>()?;

                    Ok(attr_kinds)
                }
                _ => Err(syn::Error::new_spanned(attrs, "unknown Odra attr"))
            })
            .unwrap()
            .unwrap();

        validate(&kinds)?;

        Ok(OdraAttribute { kinds })
    }
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
enum AttrKind {
    Constructor,
    Entrypoint,
    Payable,
    NonReentrant,
    Using(Vec<String>)
}

impl TryFrom<syn::NestedMeta> for AttrKind {
    type Error = syn::Error;

    fn try_from(nested_meta: syn::NestedMeta) -> Result<Self, Self::Error> {
        match nested_meta {
            syn::NestedMeta::Meta(meta) => match &meta {
                syn::Meta::Path(path) => {
                    path.try_to_string(&meta)
                        .and_then(|ident| match ident.as_str() {
                            "init" => Ok(AttrKind::Constructor),
                            "payable" => Ok(AttrKind::Payable),
                            "entrypoint" => Ok(AttrKind::Entrypoint),
                            "non_reentrant" => Ok(AttrKind::NonReentrant),
                            _ => Err(AttrKindError::Path(&meta).into())
                        })
                }
                syn::Meta::List(_) => Err(AttrKindError::List(&meta).into()),
                syn::Meta::NameValue(name_value) => {
                    let delegated_fields = match &name_value.lit {
                        Lit::Str(str) => str
                            .value()
                            .split(',')
                            .map(|str| str.trim().to_string())
                            .collect::<Vec<_>>(),
                        _ => return Err(AttrKindError::NameValue(&meta).into())
                    };
                    name_value
                        .path
                        .try_to_string(&meta)
                        .and_then(|ident| match ident.as_str() {
                            "using" => Ok(AttrKind::Using(delegated_fields)),
                            _ => Err(AttrKindError::NameValue(&meta).into())
                        })
                }
            },
            syn::NestedMeta::Lit(_) => Err(AttrKindError::Lit(&nested_meta).into())
        }
    }
}

trait TryToString {
    fn try_to_string<T: ToTokens>(&self, span: &T) -> Result<String, syn::Error>;
}

impl TryToString for Path {
    fn try_to_string<T: ToTokens>(&self, span: &T) -> Result<String, syn::Error> {
        self.get_ident()
            .map(Ident::to_string)
            .ok_or_else(|| syn::Error::new_spanned(span, "unknown Odra attribute argument (path)"))
    }
}

enum AttrKindError<'a, T> {
    Lit(&'a T),
    List(&'a T),
    Path(&'a T),
    NameValue(&'a T)
}

impl<T: ToTokens> AttrKindError<'_, T> {
    fn span(&self) -> &T {
        match self {
            AttrKindError::Lit(span) => span,
            AttrKindError::List(span) => span,
            AttrKindError::Path(span) => span,
            AttrKindError::NameValue(span) => span
        }
    }
}

impl<T: ToTokens> From<AttrKindError<'_, T>> for syn::Error {
    fn from(value: AttrKindError<'_, T>) -> Self {
        let ty = match value {
            AttrKindError::Lit(_) => "literal",
            AttrKindError::List(_) => "list",
            AttrKindError::Path(_) => "path",
            AttrKindError::NameValue(_) => "name = value"
        };
        syn::Error::new_spanned(
            value.span(),
            format!("unknown Odra attribute argument ({})", ty)
        )
    }
}

fn ensure_no_duplicates(attrs: &[AttrKind]) -> Result<(), syn::Error> {
    let mut set: HashSet<&AttrKind> = HashSet::new();

    let contains_duplicate = attrs.iter().any(|attr| !set.insert(attr));
    match contains_duplicate {
        true => Err(syn::Error::new(
            Span::call_site(),
            "attr duplicate encountered".to_string()
        )),
        false => Ok(())
    }
}

fn validate(attrs: &[AttrKind]) -> Result<(), syn::Error> {
    let mut has_constructor = false;
    let mut has_payable = false;
    attrs.iter().for_each(|attr| match attr {
        AttrKind::Constructor => has_constructor = true,
        AttrKind::Payable => has_payable = true,
        _ => {}
    });
    if has_constructor && has_payable {
        return Err(syn::Error::new(
            Span::call_site(),
            "constructor cannot be payable".to_string()
        ));
    }

    ensure_no_duplicates(attrs)
}

pub fn partition_attributes<I>(
    attrs: I
) -> Result<(Vec<OdraAttribute>, Vec<syn::Attribute>), syn::Error>
where
    I: IntoIterator<Item = syn::Attribute>
{
    let (odra_attrs, other_attrs): (Vec<OdraAttribute>, Vec<syn::Attribute>) = attrs
        .into_iter()
        .map(<Attribute as TryFrom<_>>::try_from)
        .collect::<Result<Vec<Attribute>, syn::Error>>()?
        .into_iter()
        .partition_map(|attr| match attr {
            Attribute::Odra(odra_attr) => Either::Left(odra_attr),
            Attribute::Other(other_attr) => Either::Right(other_attr)
        });

    let attrs = odra_attrs
        .clone()
        .into_iter()
        .flat_map(|attr| attr.kinds)
        .collect::<Vec<_>>();
    validate(&attrs)?;
    Ok((odra_attrs, other_attrs))
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn constructor_attr_works() {
        let expected_value = Attribute::Odra(OdraAttribute {
            kinds: vec![AttrKind::Constructor]
        });
        assert_attribute_try_from(
            syn::parse_quote! {
                #[odra(init)]
            },
            Ok(expected_value)
        );
    }

    #[test]
    fn using_attr_works() {
        let expected_value = Attribute::Odra(OdraAttribute {
            kinds: vec![AttrKind::Using(vec![
                String::from("self.value"),
                String::from("self.module"),
            ])]
        });
        assert_attribute_try_from(
            syn::parse_quote! {
                #[odra(using = "self.value, self.module")]
            },
            Ok(expected_value)
        );
    }

    #[test]
    fn payable_attr_works() {
        let expected_value = Attribute::Odra(OdraAttribute {
            kinds: vec![AttrKind::Payable]
        });
        assert_attribute_try_from(
            syn::parse_quote! {
                #[odra(payable)]
            },
            Ok(expected_value)
        );
    }

    #[test]
    fn constructor_cannot_be_payable() {
        assert_attribute_try_from(
            syn::parse_quote! {
                #[odra(init, payable)]
            },
            Err("constructor cannot be payable")
        );

        assert_attributes_try_from(
            vec![
                syn::parse_quote! { #[odra(init)] },
                syn::parse_quote! { #[odra(payable)] },
            ],
            Err("constructor cannot be payable")
        );
    }

    #[test]
    fn non_odra_attr_works() {
        let expected_value: syn::Attribute = syn::parse_quote! {
            #[yoyo(abc)]
        };
        assert_attribute_try_from(expected_value.clone(), Ok(Attribute::Other(expected_value)));
    }

    #[test]
    fn duplicated_attrs_fail() {
        assert_attribute_try_from(
            syn::parse_quote! {
                #[odra(init, init)]
            },
            Err("attr duplicate encountered")
        );

        assert_attributes_try_from(
            vec![
                syn::parse_quote! { #[odra(init)] },
                syn::parse_quote! { #[odra(init)] },
            ],
            Err("attr duplicate encountered")
        )
    }

    fn assert_attribute_try_from(input: syn::Attribute, expected: Result<Attribute, &'static str>) {
        assert_eq!(
            <Attribute as TryFrom<_>>::try_from(input).map_err(|err| err.to_string()),
            expected.map_err(ToString::to_string),
        );
    }

    fn assert_attributes_try_from(
        inputs: Vec<syn::Attribute>,
        expected: Result<(Vec<Attribute>, Vec<syn::Attribute>), &'static str>
    ) {
        let result = partition_attributes(inputs)
            .map(|(odra_attrs, other_attrs)| {
                (
                    odra_attrs
                        .into_iter()
                        .map(Attribute::from)
                        .collect::<Vec<_>>(),
                    other_attrs
                )
            })
            .map_err(|err| err.to_string());
        assert_eq!(result, expected.map_err(ToString::to_string));
    }
}