mocktopus_macros 0.3.6

Mocktopus procedural macros
use std::mem;
use syn::{AngleBracketedGenericArguments, GenericArgument, ParenthesizedGenericArguments, PathArguments, PathSegment,
          TypeBareFn, TypeParamBound, TypePath, TypeReference, ReturnType, Type};
use syn::punctuated::{Pair, Punctuated};

/// The lifetime remover is needed in cases, where Rust can't reason well enough about lifetimes of generics.
///
/// Example:
/// ```
/// trait Trait<T> {
///     fn method() -> &'static str {
///         "method"
///     }
///
///     fn another() -> &'static str;
/// }
///
/// struct Struct;
///
/// impl<'a, T> Trait<&'a T> for Struct {
///     fn another() -> &'static str {
///         //<Self as Trait<&'a T>>::method()  // FAILS TO COMPILE
///         <Self as Trait<&T>>::method()       // COMPILES FINE
///     }
/// }
/// ```
/// This is exactly the behavior that is needed in code generated by Mocktopus. It builds full UFC call to trait method
/// of struct with the same generics params. Right now copying trait name from impl header `Trait<&'a T>` and putting it
/// in `<Self as Trait<&'a T>>::method()` confuses the compiler. It can't find out, that `&'a T` implies T: 'a` and
/// complains, that `T` may not live as long as `'a`. The workaround is to remove lifetimes from generic parameters and
/// make it `<Self as Trait<&T>>::method()`. It works, because Rust can prove this statement to be safe and lifetimes
/// are dropped during monomorphisation anyway.
///
/// It seems that https://github.com/rust-lang/rust/issues/44493 solves exactly the hole in Rust's
/// lifetime reasoning, which should make lifetime remover obsolete.

pub fn remove_lifetimes_from_path<T>(path: &mut Punctuated<PathSegment, T>) {
    for path_segment in path {
        match path_segment.arguments {
            PathArguments::AngleBracketed(ref mut args) => remove_lifetimes_from_angle_bracketed_arguments(args),
            PathArguments::Parenthesized(ref mut args)  => remove_lifetimes_from_parenthesized_arguments(args),
            PathArguments::None                         => (),
        }
    }
}

fn remove_lifetimes_from_angle_bracketed_arguments(generic_arguments: &mut AngleBracketedGenericArguments) {
    filter_map_punctuated(&mut generic_arguments.args, |mut generic_argument| {
        match generic_argument {
            GenericArgument::Lifetime(_)            => return None,
            GenericArgument::Type(ref mut type_)    => remove_lifetimes_from_type(type_),
            _                                       => (),
        };
        Some(generic_argument)
    });
}

fn remove_lifetimes_from_parenthesized_arguments(generic_arguments: &mut ParenthesizedGenericArguments) {
    modify_punctuated(&mut generic_arguments.inputs, remove_lifetimes_from_type);
    if let ReturnType::Type(_, ref mut type_box) = generic_arguments.output {
        remove_lifetimes_from_type(type_box)
    }
}

fn remove_lifetimes_from_type(type_: &mut Type) {
    match *type_ {
        Type::Slice(ref mut slice)              => remove_lifetimes_from_type(&mut slice.elem),
        Type::Array(ref mut array)              => remove_lifetimes_from_type(&mut array.elem),
        Type::Ptr(ref mut ptr)                  => remove_lifetimes_from_type(&mut ptr.elem),
        Type::Reference(ref mut reference)      => remove_lifetimes_from_type_reference(reference),
        Type::BareFn(ref mut bare_fn)           => remove_lifetimes_from_type_bare_fn(bare_fn),
        Type::Never(_)                          => (),
        Type::Tuple(ref mut tuple)              => modify_punctuated(&mut tuple.elems, remove_lifetimes_from_type),
        Type::Path(ref mut path)                => remove_lifetimes_from_type_path(path),
        Type::TraitObject(ref mut trait_object) => remove_lifetimes_from_type_param_bounds(&mut trait_object.bounds),
        Type::ImplTrait(ref mut impl_trait)     => remove_lifetimes_from_type_param_bounds(&mut impl_trait.bounds),
        Type::Paren(ref mut paren)              => remove_lifetimes_from_type(&mut paren.elem),
        Type::Group(ref mut group)              => remove_lifetimes_from_type(&mut group.elem),
        Type::Infer(_)                          => (),
        Type::Macro(_)                          => (),
        Type::Verbatim(_)                       => (),
    }
}

fn remove_lifetimes_from_type_reference(type_reference: &mut TypeReference) {
    type_reference.lifetime.take();
    remove_lifetimes_from_type(&mut type_reference.elem);
}

fn remove_lifetimes_from_type_bare_fn(type_bare_fn: &mut TypeBareFn) {
    type_bare_fn.lifetimes.take();
    modify_punctuated(&mut type_bare_fn.inputs, |t| remove_lifetimes_from_type(&mut t.ty));
    if let ReturnType::Type(_, ref mut type_box) = type_bare_fn.output {
        remove_lifetimes_from_type(type_box)
    }
}

fn remove_lifetimes_from_type_path(type_path: &mut TypePath) {
    type_path.qself.as_mut().map(|q| remove_lifetimes_from_type(&mut q.ty));
    remove_lifetimes_from_path(&mut type_path.path.segments);
}

fn remove_lifetimes_from_type_param_bounds<T>(type_param_bounds: &mut Punctuated<TypeParamBound, T>,) {
    filter_map_punctuated(type_param_bounds, |type_param_bound|
        match type_param_bound {
            TypeParamBound::Trait(mut trait_bound) => {
                trait_bound.lifetimes.take();
                remove_lifetimes_from_path(&mut trait_bound.path.segments);
                Some(TypeParamBound::Trait(trait_bound))
            },
            TypeParamBound::Lifetime(_) => None,
        });
}

fn modify_punctuated<T, P, F: Fn(&mut T)>(punctuated: &mut Punctuated<T, P>, modifier: F) {
    punctuated.iter_mut().for_each(modifier)
}

fn filter_map_punctuated<T, P, F: Fn(T) -> Option<T>>(punctuated: &mut Punctuated<T, P>, filter_map: F) {
    let has_no_trailing_punct = !punctuated.empty_or_trailing();
    let filtered_iter = mem::replace(punctuated, Punctuated::new())
        .into_pairs()
        .filter_map(|p| match p {
            Pair::Punctuated(t, p)  => filter_map(t).map(|t| Pair::Punctuated(t, p)),
            Pair::End(t)            => filter_map(t).map(|t| Pair::End(t)),
        });
    punctuated.extend(filtered_iter);
    if has_no_trailing_punct && punctuated.trailing_punct() {
        if let Some(pair) = punctuated.pop() {
            punctuated.push_value(pair.into_value())
        }
    }
}

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

    mod remove_lifetimes_from_path {
        use super::*;
        use quote::ToTokens;
        use syn::{self, Path};

        fn assert_remove_lifetimes(expected: &str, input_path: &str) {
            let test_name = format!("for input path '{}'", input_path);
            let mut path: Path = syn::parse_str(input_path).expect(&test_name);

            remove_lifetimes_from_path(&mut path.segments);

            let actual = path.into_tokens().to_string().replace(" ", "");
            assert_eq!(expected, actual, "{}", test_name);
        }

        #[test]
        fn remove_lifetimes_from_path_tests() {
            assert_remove_lifetimes("Trait", "Trait");
            assert_remove_lifetimes("Trait<>", "Trait<'a>");
            assert_remove_lifetimes("Trait<[&T]>", "Trait<[&'a T]>");
            assert_remove_lifetimes("Trait<[&T;2]>", "Trait<[&'a T;2]>");
            assert_remove_lifetimes("Trait<*mut&T>", "Trait<*mut &'a T>");
            assert_remove_lifetimes("Trait<&T>", "Trait<&'a T>");
            assert_remove_lifetimes("Trait<fn(&T)->&T>", "Trait<for<'a>fn(&'a T)->&'a T>");
            assert_remove_lifetimes("Trait<(&T,)>", "Trait<(&'a T,)>");
            assert_remove_lifetimes("Trait<Fn(&T)->&T>", "Trait<Fn(&'a T)->&'a T>");
        }
    }
}