use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::spanned::Spanned;
use syn::{GenericArgument, Ident, PathArguments, PathSegment, TraitItemType, Type};
use crate::match_assoc_type;
use crate::syn_utils::{iter_type, lifetime_bounds, trait_bounds};
use crate::trait_sig::TypeTransform;
#[derive(Debug)]
pub enum AssocTypeError {
NoTraitBound,
TraitBoundContainsAssocType,
GenericAssociatedType,
}
#[derive(Debug, Clone)]
pub struct BoxType {
pub inner: TokenStream,
pub placeholder_lifetime: bool,
}
impl ToTokens for BoxType {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let inner = &self.inner;
match self.placeholder_lifetime {
true => tokens.extend(quote! {Box<#inner + '_>}),
false => tokens.extend(quote! {Box<#inner>}),
}
}
}
pub enum DestType {
Into(Type),
Box(BoxType),
}
impl DestType {
pub fn get_dest(&self) -> Type {
match self {
DestType::Into(ty) => (*ty).clone(),
DestType::Box(b) => Type::Verbatim(quote!(#b)),
}
}
pub fn type_transformation(&self) -> TypeTransform {
match self {
DestType::Into(_) => TypeTransform::Into,
DestType::Box(b) => TypeTransform::Box(b.clone()),
}
}
}
pub fn parse_assoc_type(
assoc_type: &TraitItemType,
) -> Result<(&Ident, DestType), (Span, AssocTypeError)> {
if let Some(bound) = trait_bounds(&assoc_type.bounds).next() {
if let PathSegment {
ident,
arguments: PathArguments::AngleBracketed(args),
} = &bound.path.segments[0]
{
if ident == "Into" && args.args.len() == 1 {
if let GenericArgument::Type(into_type) = &args.args[0] {
if iter_type(into_type).any(match_assoc_type) {
return Err((
into_type.span(),
AssocTypeError::TraitBoundContainsAssocType,
));
}
if !assoc_type.generics.params.is_empty() {
return Err((
assoc_type.generics.params.span(),
AssocTypeError::GenericAssociatedType,
));
}
return Ok((&assoc_type.ident, DestType::Into(into_type.clone())));
}
}
}
let bounds = &assoc_type.bounds;
return Ok((
&assoc_type.ident,
DestType::Box(BoxType {
inner: quote! {dyn #bounds},
placeholder_lifetime: !lifetime_bounds(&assoc_type.bounds)
.any(|l| l.ident == "static"),
}),
));
}
Err((assoc_type.span(), AssocTypeError::NoTraitBound))
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::{TraitItemType, Type};
use crate::parse_assoc_type::{parse_assoc_type, AssocTypeError, DestType};
#[test]
fn ok() {
let type1: TraitItemType = syn::parse2(quote! {
type A: Into<String>;
})
.unwrap();
assert!(matches!(
parse_assoc_type(&type1),
Ok((id, DestType::Into(Type::Path(path))))
if id == "A" && path.path.is_ident("String")
));
}
#[test]
fn err_no_bound() {
let type1: TraitItemType = syn::parse2(quote! {
type A;
})
.unwrap();
assert!(matches!(
parse_assoc_type(&type1),
Err((_, AssocTypeError::NoTraitBound))
));
}
#[test]
fn err_assoc_type_in_bound() {
let type1: TraitItemType = syn::parse2(quote! {
type A: Into<Self::B>;
})
.unwrap();
assert!(matches!(
parse_assoc_type(&type1),
Err((_, AssocTypeError::TraitBoundContainsAssocType))
));
}
#[test]
fn err_gat_type() {
let type1: TraitItemType = syn::parse2(quote! {
type A<X>: Into<Foobar<X>>;
})
.unwrap();
assert!(matches!(
parse_assoc_type(&type1),
Err((_, AssocTypeError::GenericAssociatedType))
));
}
#[test]
fn err_gat_lifetime() {
let type1: TraitItemType = syn::parse2(quote! {
type A<'a>: Into<Foobar<'a>>;
})
.unwrap();
assert!(matches!(
parse_assoc_type(&type1),
Err((_, AssocTypeError::GenericAssociatedType))
));
}
}