typhoon-syn 0.3.0

Syntax tree utilities and helpers for macro processing
Documentation
use {
    crate::{constraints::Constraints, Docs},
    proc_macro2::Span,
    syn::{
        parse_quote,
        visit::{visit_path_segment, Visit},
        Field, Ident, PathSegment,
    },
};

#[derive(Default, Clone)]
pub struct AccountMeta {
    pub is_signer: bool,
    pub is_mutable: bool,
    pub is_optional: bool,
}

#[derive(Clone)]
pub struct InstructionAccount {
    pub name: Ident,
    pub constraints: Constraints,
    pub meta: AccountMeta,
    pub docs: Vec<String>,
    ty: Option<PathSegment>,
    pub inner_ty: Ident,
}

impl InstructionAccount {
    pub fn get_ty(&self) -> PathSegment {
        let inner_ty = &self.inner_ty;
        self.ty.clone().unwrap_or(parse_quote!(#inner_ty<'info>))
    }
}

impl TryFrom<&Field> for InstructionAccount {
    type Error = syn::Error;

    fn try_from(value: &Field) -> Result<Self, Self::Error> {
        let mut acc = InstructionAccount {
            name: value
                .ident
                .clone()
                .ok_or(syn::Error::new_spanned(value, "The field need to be named"))?,
            docs: Docs::from(value.attrs.as_slice()).into_vec(),
            constraints: Constraints::try_from(value.attrs.as_slice())?,
            meta: AccountMeta::default(),
            ty: None,
            inner_ty: Ident::new("UncheckedAccount", Span::call_site()),
        };

        acc.visit_type(&value.ty);
        Ok(acc)
    }
}

impl Visit<'_> for InstructionAccount {
    fn visit_path_segment(&mut self, i: &syn::PathSegment) {
        let ty = i.ident.to_string();

        if ty == "Option" {
            self.meta.is_optional = true;
            visit_path_segment(self, i);
        } else if ty.starts_with("Mut") {
            self.meta.is_mutable = true;
            if self.ty.is_none() {
                self.ty = Some(i.clone());
            }
            visit_path_segment(self, i);
        } else if ty.ends_with("Signer") {
            self.meta.is_signer = true;
            if self.ty.is_none() {
                self.ty = Some(i.clone());
            }
            visit_path_segment(self, i);
        } else {
            if self.ty.is_none() {
                self.ty = Some(i.clone());
            }
            self.inner_ty = i.ident.clone();
            visit_path_segment(self, i);
        }
    }
}

#[cfg(test)]
mod tests {
    use {super::*, syn::parse_quote};

    #[test]
    fn test_instruction_parse_with_context() {
        let field: syn::Field = parse_quote!(pub random2: Option<Mut<Account<Random2>>>);
        let account = InstructionAccount::try_from(&field).unwrap();
        assert_eq!(account.inner_ty, "Random2");
        assert!(account.meta.is_mutable);
        assert!(account.meta.is_optional);
        assert!(!account.meta.is_signer);

        let field: syn::Field = parse_quote!(pub random2: UncheckedSigner<'info, Account<Random2>>);
        let account = InstructionAccount::try_from(&field).unwrap();
        assert_eq!(account.inner_ty, "Random2");
        assert!(!account.meta.is_mutable);
        assert!(!account.meta.is_optional);
        assert!(account.meta.is_signer);

        let field: syn::Field = parse_quote!(pub random2: Signer<'info, Account<Random2>>);
        let account = InstructionAccount::try_from(&field).unwrap();
        assert_eq!(account.inner_ty, "Random2");
        assert!(!account.meta.is_mutable);
        assert!(!account.meta.is_optional);
        assert!(account.meta.is_signer);

        let field: syn::Field = parse_quote!(pub random2: Account<TokenAccount>);
        let account = InstructionAccount::try_from(&field).unwrap();
        assert_eq!(account.inner_ty, "TokenAccount");
        assert!(!account.meta.is_mutable);
        assert!(!account.meta.is_optional);
        assert!(!account.meta.is_signer);

        let field: syn::Field = parse_quote!(pub random2: UncheckedAccount);
        let account = InstructionAccount::try_from(&field).unwrap();
        assert_eq!(account.inner_ty, "UncheckedAccount");
        assert!(!account.meta.is_mutable);
        assert!(!account.meta.is_optional);
        assert!(!account.meta.is_signer);
    }
}