aboutsummaryrefslogtreecommitdiff
path: root/src/parse_assoc_type.rs
blob: 37fd78cdfcab90d38551bfdb9528b52f077e02f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use proc_macro2::Span;
use syn::spanned::Spanned;
use syn::{GenericArgument, Ident, PathArguments, PathSegment, TraitItemType, Type};

use crate::syn_utils::{find_in_type, trait_bounds};
use crate::AssocTypeMatcher;

#[derive(Debug)]
pub enum AssocTypeParseError {
    AssocTypeInBound,
    GenericAssociatedType,
    NoIntoBound,
}

pub fn parse_assoc_type(
    assoc_type: &TraitItemType,
) -> Result<(&Ident, &Type), (Span, AssocTypeParseError)> {
    for bound in trait_bounds(&assoc_type.bounds) {
        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<Self::B>
                    if find_in_type(into_type, &AssocTypeMatcher).is_some() {
                        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, into_type));
                }
            }
        }
    }
    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};

    #[test]
    fn ok() {
        let type1: TraitItemType = syn::parse2(quote! {
            type A: Into<String>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Ok((id, 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<Self::B>;
        })
        .unwrap();

        assert!(matches!(
            parse_assoc_type(&type1),
            Err((_, AssocTypeParseError::AssocTypeInBound))
        ));
    }

    #[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((_, AssocTypeParseError::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((_, AssocTypeParseError::GenericAssociatedType))
        ));
    }
}