fluid_attributes 0.4.0

Proc macro attributes for the fluid crate.
Documentation
use crate::{body, helpers::add_test_attribute, theory};
use pm::TokenStream;
use quote::{quote, ToTokens};
use std::{iter::FromIterator, mem::replace};
use syn::{
    parse::Error, parse_quote, punctuated::Punctuated, spanned::Spanned, visit_mut::VisitMut,
    FnArg, ImplItem, ImplItemMethod, ItemFn, ItemImpl, Path, Type,
};

type FnArgs = Punctuated<syn::FnArg, syn::token::Comma>;
type CallArgs = Punctuated<syn::Pat, syn::token::Comma>;

pub fn generate(mut input: ItemImpl) -> Result<TokenStream, Error> {
    assert_this_is_an_inherent_impl(&input)?;
    let ty = input.self_ty.clone();
    let mut output = TokenStream::new();

    for func in input
        .items
        .iter_mut()
        .map(|item| testable_function(item, &ty))
    {
        if let Some(func) = func? {
            output.extend(func)
        }
    }
    output.extend(input.into_token_stream());

    Ok(output)
}

/// Checks that the `session` attribute is not a trait implementation.
fn assert_this_is_an_inherent_impl(input: &ItemImpl) -> Result<(), Error> {
    match &input.trait_ {
        None => Ok(()),
        Some((_, path, _)) => Err(Error::new(
            path.span(),
            "The `session` attribute must be used over an inherent implementation",
        )),
    }
}

/// Modifies the method in place, and returns the generated test code: module for a theory,
/// function for a fact.
fn testable_function(item: &mut ImplItem, ty: &Type) -> Result<Option<TokenStream>, Error> {
    enum TypeTest {
        Fact,
        Theory,
    }

    impl TypeTest {
        fn from_path(path: &Path) -> Option<TypeTest> {
            if path.is_ident("fact") {
                Some(TypeTest::Fact)
            } else if path.is_ident("theory") {
                Some(TypeTest::Theory)
            } else {
                None
            }
        }
    }

    /// Return whether an item in an implementation block is a test method.
    fn is_test_method(
        item: &mut syn::ImplItem,
    ) -> Result<Option<(TypeTest, &mut ImplItemMethod)>, Error> {
        let out = if let ImplItem::Method(method) = item {
            let test_attr = method
                .attrs
                .iter()
                .position(|attr| TypeTest::from_path(&attr.path).is_some())
                .map(|i| method.attrs.remove(i));

            // Checks that the method has `self` as 1st argument:
            if test_attr.is_some() {
                fn is_self_by_value(arg: &FnArg) -> bool {
                    if let FnArg::SelfValue(_) = arg {
                        true
                    } else {
                        false
                    }
                }
                match method.sig.decl.inputs.iter().next() {
                    Some(first_arg) if is_self_by_value(first_arg) => (),
                    _ => Err(Error::new(
                        method.sig.ident.span(),
                        "This method must take `Self` by value to be testable",
                    ))?,
                }
            }
            test_attr
                .and_then(|attr| TypeTest::from_path(&attr.path))
                .map(|tytest| (tytest, method))
        } else {
            None
        };

        Ok(out)
    }

    fn generate_test_func_from_method(
        method: &mut ImplItemMethod,
        ty: &Type,
    ) -> Result<ItemFn, Error> {
        fn creates_the_call_args(params: &FnArgs) -> Result<CallArgs, Error> {
            const ERRMSG: &str = "Expected a regular argument";
            let args = params.iter().map(|fnarg| match fnarg {
                FnArg::SelfRef(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
                FnArg::SelfValue(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
                FnArg::Inferred(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
                FnArg::Ignored(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
                FnArg::Captured(arg) => Ok(arg.pat.clone()),
            });
            args.collect()
        }

        let ident = &method.sig.ident;
        let mut decl = method.sig.decl.clone();
        // Remove the `self` parameter.
        decl.inputs = Punctuated::from_iter(decl.inputs.into_iter().skip(1));
        add_test_attribute(&mut method.attrs);

        let args = creates_the_call_args(&decl.inputs)?;
        let func = ItemFn {
            attrs: replace(&mut method.attrs, Default::default()),
            vis: method.vis.clone(),
            constness: None,
            unsafety: None,
            asyncness: None,
            abi: None,
            ident: ident.clone(),
            decl: Box::new(decl),
            block: Box::new(parse_quote! {{
                #ty::default().#ident(#args)
            }}),
        };

        body::Transform::new(None).visit_block_mut(&mut method.block);
        Ok(func)
    }

    match is_test_method(item)? {
        Some((TypeTest::Fact, method)) => {
            generate_test_func_from_method(method, ty).map(|f| Some(f.into_token_stream()))
        }
        Some((TypeTest::Theory, method)) => theory::generate(
            method.sig.ident.clone(),
            &mut method.sig.decl,
            &mut method.block,
            &mut method.attrs,
            Some(ty),
        )
        .map(|f| Some(f.into_token_stream())),
        None => Ok(None),
    }
}