bounded-static-derive-more 0.1.0

Macro to derive ToBoundedStatic and IntoBoundedStatic traits
Documentation
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
    ConstParam, Field, GenericParam, Generics, Ident, Lifetime, PredicateType, Type, TypeParam,
    WhereClause, WherePredicate, parse_quote,
};

pub(super) fn has_non_static_lifetime(ty: &Type, generic_idents: &[&Ident]) -> bool {
    match ty {
        Type::Array(array) => has_non_static_lifetime(&array.elem, generic_idents),
        Type::Paren(paren) => has_non_static_lifetime(&paren.elem, generic_idents),
        Type::Path(syn::TypePath { qself: None, path }) => {
            if path.segments.len() == 1
                && path
                    .segments
                    .first()
                    .is_some_and(|segment| segment.arguments.is_empty())
            {
                path.segments
                    .first()
                    .is_none_or(|segment| generic_idents.contains(&&segment.ident))
            } else {
                path.segments
                    .iter()
                    .any(|segment| match &segment.arguments {
                        syn::PathArguments::None => false,
                        syn::PathArguments::AngleBracketed(arguments) => {
                            arguments.args.iter().any(|argument| match argument {
                                syn::GenericArgument::Type(ty) => {
                                    has_non_static_lifetime(ty, generic_idents)
                                }
                                syn::GenericArgument::AssocType(assoc_type) => {
                                    has_non_static_lifetime(&assoc_type.ty, generic_idents)
                                }
                                syn::GenericArgument::Lifetime(syn::Lifetime { ident, .. }) => {
                                    *ident != "static"
                                }
                                _ => true,
                            })
                        }
                        syn::PathArguments::Parenthesized(_) => true,
                    })
            }
        }

        Type::Tuple(tuple) => tuple
            .elems
            .iter()
            .any(|elem| has_non_static_lifetime(elem, generic_idents)),
        _ => true,
    }
}

pub(super) fn generic_idents(generics: &Generics) -> Vec<&Ident> {
    generics
        .params
        .iter()
        .filter_map(|param| match param {
            GenericParam::Type(type_param) => Some(&type_param.ident),
            _ => None,
        })
        .collect()
}

/// The method and trait bound for both traits we will generate.
#[derive(Copy, Clone)]
pub(super) enum TargetTrait {
    ToBoundedStatic,
    IntoBoundedStatic,
}

impl TargetTrait {
    pub fn method(self) -> Ident {
        match self {
            Self::ToBoundedStatic => format_ident!("to_static"),
            Self::IntoBoundedStatic => format_ident!("into_static"),
        }
    }

    pub fn bound(self) -> Ident {
        match self {
            Self::ToBoundedStatic => format_ident!("ToBoundedStatic"),
            Self::IntoBoundedStatic => format_ident!("IntoBoundedStatic"),
        }
    }

    pub const fn needs_clone(self) -> bool {
        match self {
            Self::ToBoundedStatic => true,
            Self::IntoBoundedStatic => false,
        }
    }
}

/// Check for references which aren't `'static` and panic.
///
/// # Examples
///
/// The following `struct` cannot be made static _for all_ lifetimes `'a` (it is only valid for the `'static` lifetime)
/// and so will fail this check:
///
/// ```compile_fail
/// #[derive(ToStatic)]
/// struct Foo<'a> {
///   bar: &'a str
/// }
/// ```
///
/// This `struct` is will also pass validation as it can be converted to `'static` _for all_ lifetimes `'a`:
///
/// ```rust
/// # use bounded_static_derive_more::ToStatic;
/// #[derive(ToStatic)]
/// struct Foo<'a> {
///   bar: std::borrow::Cow<'a, str>
/// }
/// ```
///
/// Note that even without this check the compilation will fail if a non-static reference is used, however by
/// performing this check we can issue a more explicit failure message to the developer.
pub(super) fn check_field(field: &Field) {
    if let Type::Reference(ty) = &field.ty
        && let Some(Lifetime { ident, .. }) = &ty.lifetime
    {
        #[allow(clippy::manual_assert)]
        if *ident != "static" {
            panic!(
                "non-static references cannot be made static: {:?}",
                quote!(#field).to_string()
            )
        }
    }
}

/// The generic parameters of the `Static` associated type for `TargetTrait`.
///
/// i.e. `Static = Foo<'static, <T as ToBoundedStatic>::Static>`
pub(super) fn make_target_generics(generics: &Generics, target: TargetTrait) -> Vec<TokenStream> {
    generics
        .params
        .iter()
        .map(|param| match param {
            GenericParam::Type(TypeParam { ident, .. }) => {
                let target_bound = target.bound();
                quote!(<#ident as ::bounded_static::#target_bound>::Static)
            }
            GenericParam::Lifetime(_) => quote!('static),
            GenericParam::Const(ConstParam { ident, .. }) => quote!(#ident),
        })
        .collect()
}

/// Make a `Generics` with generic bounds for `TargetTrait`.
///
/// # Examples
///
/// Given the following struct:
///
/// ```rust
/// # use std::borrow::Cow;
/// struct Baz<'a, T: Into<String> + 'a> {
///     t: T,
///     r: Cow<'a, str>,
///  }
/// ```
///
/// We wish to produce (for example for `ToBoundedStatic`, similar for `IntoBoundedStatic`):
///
/// ```rust
/// # use std::borrow::Cow;
/// # struct Baz<'a, T: Into<String> + 'a> {
/// #    t: T,
/// #    r: Cow<'a, str>,
/// # }
/// impl<'a, T: Into<String> + 'a + ::bounded_static::ToBoundedStatic> ::bounded_static::ToBoundedStatic for Baz<'a, T>
/// where
///     <T as ::bounded_static::ToBoundedStatic>::Static: Into<String> + 'a {
///
///     type Static = Baz<'static, <T as ::bounded_static::ToBoundedStatic>::Static>;
///
///     fn to_static(&self) -> Self::Static {
///         Baz { t: self.t.to_static(), r: self.r.to_static() }
///     }
/// }
/// ```
///
/// In the above example we can see that `T` has the bound `Into<String> + 'a` and therefore:
///
/// - Generic parameter `T` has the additional bound `::bounded_static::ToBoundedStatic`
/// - Associated type `T::Static` has the bound of `T`, i.e. `Into<String> + 'a`
pub(super) fn make_bounded_generics(generics: &Generics, target: TargetTrait) -> Generics {
    let params = make_bounded_generic_params(generics, target);
    let predicates = make_bounded_generic_predicates(generics, target);
    let static_predicates = make_static_generic_predicates(generics, target);
    let where_items: Vec<_> = predicates.into_iter().chain(static_predicates).collect();
    Generics {
        params: parse_quote!(#(#params),*),
        where_clause: Some(parse_quote!(where #(#where_items),* )),
        ..*generics
    }
}

/// Make generic parameters bound by `TargetTrait`.
///
/// i.e. given parameter `T: Into<String>` create `T: Into<String> + ::bounded_static::TargetTrait`
fn make_bounded_generic_params(generics: &Generics, target: TargetTrait) -> Vec<GenericParam> {
    generics
        .params
        .iter()
        .map(|param| match param {
            GenericParam::Type(ty) => GenericParam::Type(ty.clone_with_bound(&target.bound())),
            other => other.clone(),
        })
        .collect()
}

/// Make generic predicates bound by `TargetTrait`.
///
/// i.e. given predicate `T: Into<String>` create `T: Into<String> + ::bounded_static::TargetTrait`
fn make_bounded_generic_predicates(
    generics: &Generics,
    target: TargetTrait,
) -> Vec<WherePredicate> {
    match generics.where_clause.as_ref() {
        Some(WhereClause { predicates, .. }) => predicates
            .iter()
            .map(|predicate| match predicate {
                WherePredicate::Type(ty) => {
                    WherePredicate::Type(ty.clone_with_bound(&target.bound()))
                }
                other => other.clone(),
            })
            .collect(),
        None => vec![],
    }
}

/// Make generic predicates for associated item `T::Static` bound as per `T`.
///
/// i.e. given:
///
/// ```rust
/// # trait Foo {}
/// struct Baz<T: Into<String>> where T: Foo {
///     t: T,
/// }
/// ```
///
/// The generated trait impl associated type `Static` must reflect the original generic bounds as well as any
/// additional bounds from a `where` clause.  For the example above the associated type bound would be
/// `<T as ToBoundedStatic>::Static: Into<String> + Foo`.
fn make_static_generic_predicates(generics: &Generics, target: TargetTrait) -> Vec<WherePredicate> {
    generics
        .params
        .iter()
        .filter_map(|param| match param {
            GenericParam::Type(param_ty) => {
                let var = &param_ty.ident;
                let param_ty_bounds = &param_ty.bounds;
                let target_bound = target.bound();
                match find_predicate(generics.where_clause.as_ref(), var) {
                    None if param_ty_bounds.is_empty() => None,
                    None => Some(parse_quote!(<#var as #target_bound>::Static: #param_ty_bounds)),
                    Some(predicate_ty) => {
                        let predicate_bounds = &predicate_ty.bounds;
                        if param_ty_bounds.is_empty() {
                            Some(parse_quote!(<#var as #target_bound>::Static: #predicate_bounds))
                        } else {
                            Some(parse_quote!(<#var as #target_bound>::Static: #param_ty_bounds + #predicate_bounds))
                        }
                    }
                }
            }
            _ => None,
        })
        .collect()
}

/// Search the given `WhereClause` for a `WherePredicate` which matches the given `Ident`.
fn find_predicate<'a>(
    where_clause: Option<&'a WhereClause>,
    var: &Ident,
) -> Option<&'a PredicateType> {
    where_clause
        .as_ref()
        .map(|WhereClause { predicates, .. }| predicates)
        .and_then(|predicate| {
            predicate.iter().find_map(|p| match p {
                WherePredicate::Type(ty) => match &ty.bounded_ty {
                    Type::Path(path) => path.path.is_ident(var).then_some(ty),
                    _ => None,
                },
                _ => None,
            })
        })
}

/// Clone and add a bound to a type.
trait CloneWithBound {
    fn clone_with_bound(&self, bound: &Ident) -> Self;
}

/// Clone and add a bound to a `PredicateType` (in a `where` clause).
impl CloneWithBound for PredicateType {
    fn clone_with_bound(&self, bound: &Ident) -> Self {
        let mut bounded = self.clone();
        bounded.bounds.push(parse_quote!(::bounded_static::#bound));
        bounded
    }
}

/// Clone and add a bound to a `TypeParam`.
impl CloneWithBound for TypeParam {
    fn clone_with_bound(&self, bound: &Ident) -> Self {
        let mut bounded = self.clone();
        bounded.bounds.push(parse_quote!(::bounded_static::#bound));
        bounded
    }
}