fluid_attributes 0.4.0

Proc macro attributes for the fluid crate.
Documentation
//! Handles the `theory` tests. The code generation works as following:
//!
//! ```
//! #[theory]
//! #[case(0, "zero")]
//! #[case(2, "two")]
//! #[case(4, "four")]
//! fn is_even_is_ok<T>(i: i32, _s: T) where T: AsRef<str> {
//!     is_even(i).should().be_true();
//! }
//! ```
//!
//! becomes:
//!
//! ```
//! mod is_even_is_ok {
//!     fn actual_test<T>(i: i32, _s: T, case_uuid: &'static str) where T: AsRef<str> {
//!         LeftElement::new(is_even(i), "is_even(i)", "file:line", Some(case_uuid)).should().be_true();
//!     }
//!
//!     pub fn case_0_zero() {
//!         actual_test(0, "zero", "i = 0; _s = \"zero\"");
//!     }
//!
//!     pub fn case_2_two() {
//!         actual_test(0, "two", "i = 2; _s = \"two\"");
//!     }
//!
//!     pub fn case_4_four() {
//!         actual_test(0, "four", "i = 4; _s = \"four\"");
//!     }
//! }
//! ```

use crate::body;
use crate::helpers::*;
use pm::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::{iter::FromIterator, mem::replace};
use syn::{
    parse::Error, parse_quote, punctuated::Punctuated, spanned::Spanned, visit_mut::VisitMut,
    Attribute, Block, FnArg, FnDecl, Ident, ItemMod, Type, Visibility,
};

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

//FIXME: when the `syn` crate unify methods and free functions, this can be simplified (maybe 0.16?).
pub fn generate(
    ident: Ident,
    decl: &mut FnDecl,
    block: &mut Block,
    attrs: &mut Vec<Attribute>,
    session: Option<&Type>,
) -> Result<ItemMod, Error> {
    // Case identifier: `case_UUID: &'static str`
    let case_ident = generate_case_ident();
    let cases = extract_cases(attrs)?;
    // A stringified representation of each case: `"i = 0"`
    let stringified_cases = generate_stringified_cases(&decl.inputs, &cases, session.is_some())?;
    let span = ident.span();
    let test_fn_idents: Vec<_> = cases.iter().map(move |s| fn_name(s, span)).collect();
    let vis = Visibility::Inherited;
    let attrs = {
        let mut attrs = replace(attrs, Vec::new());
        add_test_attribute(&mut attrs);

        TokenStream::from_iter(attrs.into_iter().flat_map(ToTokens::into_token_stream))
    };

    // Adds the new case parameter to the base function
    decl.inputs.push(parse_quote!(#case_ident: &'static str));
    body::Transform::new(Some(case_ident)).visit_block_mut(block);

    let tests =
        zip2(test_fn_idents, cases, stringified_cases).map(|(case_ident, case, stringified)| {
            match session {
                Some(ty) => quote! {
                    #attrs
                    pub fn #case_ident() {
                        #ty::default().#ident(#case, #stringified)
                    }
                },
                None => quote! {
                    #attrs
                    pub fn #case_ident() {
                        #ident(#case, #stringified)
                    }
                },
            }
        });
    let output = parse_quote! {
        #vis mod #ident {
            use super::*;

            #( #tests )*
        }
    };

    Ok(output)
}

/// Creates an identifier for a given case from its parameters.
fn fn_name(args: &CallArgs, span: Span) -> Ident {
    fn remove_dup(mut s: String) -> String {
        let mut prev = None;

        s.retain(|c| {
            let result = match &prev {
                Some(prev) if *prev == '_' && c == '_' => false,
                _ => true,
            };
            prev = Some(c);
            result
        });

        s
    }

    let s = args
        .iter()
        .cloned()
        .fold(String::from("case"), |s, arg| {
            format!("{}_{}", s, arg.into_token_stream())
        })
        .replace(|c| !char::is_alphanumeric(c), "_")
        .to_lowercase();

    Ident::new(remove_dup(s).trim_end_matches('_'), span)
}

/// Generates a string representation of a list of arguments.
fn generate_stringified_cases(
    args: &FnArgs,
    cases: &[CallArgs],
    skip_self: bool,
) -> Result<Vec<String>, Error> {
    let args = {
        let mut output = Vec::new();
        for arg in args.iter().skip(skip_self as usize) {
            // If the first argument is a `self` parameter, it must be skipped
            match arg {
                FnArg::Captured(arg) => {
                    output.push(format!("{}", arg.pat.clone().into_token_stream()))
                }
                _ => Err(Error::new(arg.span(), "cannot format this argument"))?,
            }
        }
        output
    };

    let stringified = cases
        .iter()
        .cloned()
        .map(|case| {
            case.iter()
                .zip(&args)
                .map(|(val, arg)| format!("{} = {}", arg, val.into_token_stream()))
                .join()
        })
        .collect();
    Ok(stringified)
}

/// Generates a new identifier to pass the case information to the test. It is
/// guaranteed to not be used by the user, thanks to the uuid crate.
fn generate_case_ident() -> Ident {
    let uuid = uuid::Uuid::new_v4();
    let ident = format!("case_{}", uuid).replace('-', "_");

    Ident::new(&ident, Span::call_site())
}

/// Extract the `case`s attributes from the attributes tied to the test function.
fn extract_cases(attrs: &mut Vec<Attribute>) -> Result<Vec<CallArgs>, Error> {
    // Put the cases at the end of the Vec
    attrs.sort_by_key(|attr| attr.path.is_ident("case"));
    let to_skip = attrs
        .iter()
        .take_while(|attr| !attr.path.is_ident("case"))
        .count();
    let mut output = Vec::new();

    for case in attrs.drain(to_skip..) {
        let args = case.tts.stream_args()?;

        output.push(parse_quote!(#args))
    }
    Ok(output)
}