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::parse_trait_sig::TypeTransform; use crate::syn_utils::{iter_type, lifetime_bounds, trait_bounds}; #[derive(Debug)] pub enum AssocTypeParseError { AssocTypeInBound, GenericAssociatedType, NoIntoBound, } #[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<'a> { Into(&'a 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, AssocTypeParseError)> { if let Some(bound) = trait_bounds(&assoc_type.bounds).next() { if let PathSegment { ident, arguments: PathArguments::AngleBracketed(args), } = bound.path.segments.first().unwrap() { if ident == "Into" && args.args.len() == 1 { if let GenericArgument::Type(into_type) = args.args.first().unwrap() { // provide a better error message for type A: Into if iter_type(into_type).any(match_assoc_type) { return Err((into_type.span(), AssocTypeParseError::AssocTypeInBound)); } // TODO: support lifetime GATs (see the currently failing tests/gats.rs) if !assoc_type.generics.params.is_empty() { return Err(( assoc_type.generics.params.span(), AssocTypeParseError::GenericAssociatedType, )); } return Ok((&assoc_type.ident, DestType::Into(into_type))); } } } 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(), AssocTypeParseError::NoIntoBound)) } #[cfg(test)] mod tests { use quote::quote; use syn::{TraitItemType, Type}; use crate::parse_assoc_type::{parse_assoc_type, AssocTypeParseError, DestType}; #[test] fn ok() { let type1: TraitItemType = syn::parse2(quote! { type A: Into; }) .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((_, AssocTypeParseError::NoIntoBound)) )); } #[test] fn err_assoc_type_in_bound() { let type1: TraitItemType = syn::parse2(quote! { type A: Into; }) .unwrap(); assert!(matches!( parse_assoc_type(&type1), Err((_, AssocTypeParseError::AssocTypeInBound)) )); } #[test] fn err_gat_type() { let type1: TraitItemType = syn::parse2(quote! { type A: Into>; }) .unwrap(); assert!(matches!( parse_assoc_type(&type1), Err((_, AssocTypeParseError::GenericAssociatedType)) )); } #[test] fn err_gat_lifetime() { let type1: TraitItemType = syn::parse2(quote! { type A<'a>: Into>; }) .unwrap(); assert!(matches!( parse_assoc_type(&type1), Err((_, AssocTypeParseError::GenericAssociatedType)) )); } }