From 684d7e5873aa0b6423e167124fd8c72e02173ba2 Mon Sep 17 00:00:00 2001
From: Martin Fischer <martin@push-f.com>
Date: Mon, 22 Nov 2021 12:57:53 +0100
Subject: better errors for type arguments of collections

---
 src/lib.rs       | 23 +++++++++++--
 src/transform.rs | 98 ++++++++++++++++++++++++++++++++++----------------------
 2 files changed, 79 insertions(+), 42 deletions(-)

(limited to 'src')

diff --git a/src/lib.rs b/src/lib.rs
index 67c1223..1a84a09 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -53,9 +53,11 @@ mod syn_utils;
 mod transform;
 
 macro_rules! abort {
-    ($span:expr, $message:literal) => {
-        quote_spanned! {$span => compile_error!($message);}.into()
-    };
+    ($span:expr, $message:literal $(,$args:tt)*) => {{
+        let msg = format!($message $(,$args)*);
+        let tokens = quote_spanned! {$span => compile_error!(#msg);}.into();
+        tokens
+    }};
 }
 
 #[proc_macro_attribute]
@@ -119,6 +121,21 @@ pub fn dynamize(_attr: TokenStream, input: TokenStream) -> TokenStream {
                     MethodError::Transform(TransformError::UnsupportedType) => {
                         return abort!(span, "dynamize does not know how to convert this type")
                     }
+                    MethodError::Transform(TransformError::ExpectedAtLeastNTypes(n)) => {
+                        return abort!(
+                            span,
+                            "dynamize expects at least {} generic type arguments for this type",
+                            n
+                        )
+                    }
+                    MethodError::Transform(TransformError::AssocTypeAfterFirstNTypes(n, ident)) => {
+                        return abort!(
+                            span,
+                            "for {} dynamize supports associated types only within the first {} generic type parameters",
+                            ident,
+                            n
+                        )
+                    }
                     MethodError::UnconvertedAssocType => {
                         return abort!(span, "dynamize does not support associated types here")
                     }
diff --git a/src/transform.rs b/src/transform.rs
index 60ac6e8..05866b6 100644
--- a/src/transform.rs
+++ b/src/transform.rs
@@ -23,26 +23,32 @@ pub struct TypeConverter<'a> {
 pub enum TransformError {
     AssocTypeWithoutDestType,
     UnsupportedType,
+    ExpectedAtLeastNTypes(usize),
+    AssocTypeAfterFirstNTypes(usize, Ident),
 }
 
 impl TypeConverter<'_> {
-    /// Returns true for types that take one generic type parameter T
-    /// and implement IntoIterator<Item=T> and FromIterator<T>
-    fn known_as_collection_with_one_type(&self, ident: &Ident) -> bool {
+    /// A return type of Some(1) means that the type implements
+    /// IntoIterator<Item=T1> and FromIterator<T1>
+    /// with T1 being its first generic type parameter.
+    ///
+    /// A return type of Some(2) means that the type implements
+    /// IntoIterator<Item=(T1, T2)> and FromIterator<(T1, T2)>
+    /// with T1 and T2 being its first two generic type parameters.
+    ///
+    /// ... etc. A return type of None means the type isn't recognized.
+    #[rustfmt::skip]
+    fn get_collection_type_count(&self, ident: &Ident) -> Option<usize> {
         // when adding a type here don't forget to document it in the README
-        ident == "Vec"
-            || ident == "VecDeque"
-            || ident == "LinkedList"
-            || ident == "HashSet"
-            || ident == "BinaryHeap"
-            || ident == "BTreeSet"
-    }
-
-    /// Returns true for types that take two generic type parameter K and V
-    /// and implement IntoIterator<Item=(K, V)> and FromIterator<(K, V)>
-    fn known_as_collection_with_two_types(&self, ident: &Ident) -> bool {
-        // when adding a type here don't forget to document it in the README
-        ident == "HashMap" || ident == "BTreeMap"
+        if ident == "Vec"        { return Some(1); }
+        if ident == "VecDeque"   { return Some(1); }
+        if ident == "LinkedList" { return Some(1); }
+        if ident == "HashSet"    { return Some(1); }
+        if ident == "BinaryHeap" { return Some(1); }
+        if ident == "BTreeSet"   { return Some(1); }
+        if ident == "HashMap"    { return Some(2); }
+        if ident == "BTreeMap"   { return Some(2); }
+        None
     }
 
     pub fn convert_type(&self, type_: &mut Type) -> Result<TypeTransform, (Span, TransformError)> {
@@ -113,36 +119,50 @@ impl TypeConverter<'_> {
                 if let PathArguments::AngleBracketed(args) = &mut last_seg.arguments {
                     let mut args: Vec<_> = type_arguments_mut(&mut args.args).collect();
 
-                    if args.len() == 1 {
-                        if iter_type(args[0]).any(match_assoc_type) {
-                            if (last_seg.ident == "Option" && path_len == 1)
-                                || last_seg.ident == "Result"
-                            {
-                                return Ok(TypeTransform::Map(self.convert_type(args[0])?.into()));
-                            } else if self.known_as_collection_with_one_type(&last_seg.ident)
-                                && path_len == 1
-                            {
-                                return Ok(TypeTransform::IntoIterMapCollect(vec![
-                                    self.convert_type(args[0])?
-                                ]));
+                    if path_len == 1 {
+                        if let Some(type_count) = self.get_collection_type_count(&last_seg.ident) {
+                            if args.len() < type_count {
+                                return Err((
+                                    last_seg.span(),
+                                    TransformError::ExpectedAtLeastNTypes(type_count),
+                                ));
+                            }
+                            for i in type_count..args.len() {
+                                if iter_type(args[i]).any(match_assoc_type) {
+                                    return Err((
+                                        args[i].span(),
+                                        TransformError::AssocTypeAfterFirstNTypes(
+                                            type_count,
+                                            last_seg.ident.clone(),
+                                        ),
+                                    ));
+                                }
+                            }
+                            let mut transforms = Vec::new();
+                            for arg in args {
+                                transforms.push(self.convert_type(arg)?);
                             }
+                            return Ok(TypeTransform::IntoIterMapCollect(transforms));
+                        }
+                    }
+
+                    if args.len() == 1 {
+                        if iter_type(args[0]).any(match_assoc_type)
+                            && ((last_seg.ident == "Option" && path_len == 1)
+                                || last_seg.ident == "Result")
+                        {
+                            return Ok(TypeTransform::Map(self.convert_type(args[0])?.into()));
                         }
                     } else if args.len() == 2
                         && path_len == 1
                         && (iter_type(args[0]).any(match_assoc_type)
                             || iter_type(args[1]).any(match_assoc_type))
+                        && last_seg.ident == "Result"
                     {
-                        if last_seg.ident == "Result" {
-                            return Ok(TypeTransform::Result(
-                                self.convert_type(args[0])?.into(),
-                                self.convert_type(args[1])?.into(),
-                            ));
-                        } else if self.known_as_collection_with_two_types(&last_seg.ident) {
-                            return Ok(TypeTransform::IntoIterMapCollect(vec![
-                                self.convert_type(args[0])?,
-                                self.convert_type(args[1])?,
-                            ]));
-                        }
+                        return Ok(TypeTransform::Result(
+                            self.convert_type(args[0])?.into(),
+                            self.convert_type(args[1])?.into(),
+                        ));
                     }
                 }
             }
-- 
cgit v1.2.3