Browse Source

fix: more accurate derive `Clone, PartialEq` for generic components (#3968)

* convert to manual clone implements
* add test for generics
Clouds 2 months ago
parent
commit
e0ecf0eb49
2 changed files with 105 additions and 5 deletions
  1. 77 5
      packages/core-macro/src/component.rs
  2. 28 0
      packages/core-macro/tests/generics.rs

+ 77 - 5
packages/core-macro/src/component.rs

@@ -1,6 +1,7 @@
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote, ToTokens, TokenStreamExt};
 use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
 use syn::*;
 
@@ -55,11 +56,13 @@ impl ToTokens for ComponentBody {
             // Props declared, so we generate a props struct and then also attach the doc attributes to it
             false => {
                 let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
-                let props_struct = self.props_struct();
+                let (props_struct, props_impls) = self.props_struct();
                 quote! {
                     #[doc = #doc]
                     #[allow(missing_docs)]
                     #props_struct
+
+                    #(#props_impls)*
                 }
             }
         };
@@ -216,7 +219,7 @@ impl ComponentBody {
     ///
     /// We try our best to transfer over any declared doc attributes from the original function signature onto the
     /// props struct fields.
-    fn props_struct(&self) -> ItemStruct {
+    fn props_struct(&self) -> (ItemStruct, Vec<ItemImpl>) {
         let ItemFn { vis, sig, .. } = &self.item_fn;
         let Signature {
             inputs,
@@ -225,17 +228,56 @@ impl ComponentBody {
             ..
         } = sig;
 
+        let generic_arguments = if !generics.params.is_empty() {
+            let generic_arguments = generics
+                .params
+                .iter()
+                .map(make_prop_struct_generics)
+                .collect::<Punctuated<_, Token![,]>>();
+            quote! { <#generic_arguments> }
+        } else {
+            quote! {}
+        };
         let where_clause = &generics.where_clause;
         let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
+        let struct_field_idents = inputs
+            .iter()
+            .map(make_prop_struct_field_idents)
+            .collect::<Vec<_>>();
         let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
 
-        parse_quote! {
-            #[derive(Props, Clone, PartialEq)]
+        let item_struct = parse_quote! {
+            #[derive(Props)]
             #[allow(non_camel_case_types)]
             #vis struct #struct_ident #generics #where_clause {
                 #(#struct_fields),*
             }
-        }
+        };
+
+        let item_impl_clone = parse_quote! {
+            impl #generics ::core::clone::Clone for #struct_ident #generic_arguments #where_clause {
+                #[inline]
+                fn clone(&self) -> Self {
+                    Self {
+                        #(#struct_field_idents: ::core::clone::Clone::clone(&self.#struct_field_idents)),*
+                    }
+                }
+            }
+        };
+
+        let item_impl_partial_eq = parse_quote! {
+            impl #generics ::core::cmp::PartialEq for #struct_ident #generic_arguments #where_clause {
+                #[inline]
+                fn eq(&self, other: &Self) -> bool {
+                    #(
+                        self.#struct_field_idents == other.#struct_field_idents &&
+                    )*
+                    true
+                }
+            }
+        };
+
+        (item_struct, vec![item_impl_clone, item_impl_partial_eq])
     }
 
     /// Convert a list of function arguments into a list of doc attributes for the props struct
@@ -494,6 +536,36 @@ fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
     }
 }
 
+/// Get ident from a function arg
+fn make_prop_struct_field_idents(f: &FnArg) -> &Ident {
+    // There's no receivers (&self) allowed in the component body
+    let FnArg::Typed(pt) = f else { unreachable!() };
+
+    match pt.pat.as_ref() {
+        // rip off mutability
+        // todo: we actually don't want any of the extra bits of the field pattern
+        Pat::Ident(f) => &f.ident,
+        _ => unreachable!(),
+    }
+}
+
+fn make_prop_struct_generics(generics: &GenericParam) -> TokenStream {
+    match generics {
+        GenericParam::Type(ty) => {
+            let ident = &ty.ident;
+            quote! { #ident }
+        }
+        GenericParam::Lifetime(lifetime) => {
+            let lifetime = &lifetime.lifetime;
+            quote! { #lifetime }
+        }
+        GenericParam::Const(c) => {
+            let ident = &c.ident;
+            quote! { #ident }
+        }
+    }
+}
+
 fn rebind_mutability(f: &FnArg) -> TokenStream {
     // There's no receivers (&self) allowed in the component body
     let FnArg::Typed(pt) = f else { unreachable!() };

+ 28 - 0
packages/core-macro/tests/generics.rs

@@ -30,6 +30,34 @@ fn generic_props_compile() {
         rsx! {}
     }
 
+    #[component]
+    fn TakesCloneArc<T: PartialEq + 'static>(value: std::sync::Arc<T>) -> Element {
+        rsx! {}
+    }
+
+    struct MyBox<T>(std::marker::PhantomData<T>);
+
+    impl<T: Display> Clone for MyBox<T> {
+        fn clone(&self) -> Self {
+            MyBox(std::marker::PhantomData)
+        }
+    }
+
+    impl<T: Display> PartialEq for MyBox<T> {
+        fn eq(&self, _: &Self) -> bool {
+            true
+        }
+    }
+
+    #[component]
+    #[allow(clippy::multiple_bound_locations)]
+    fn TakesCloneMyBox<T: 'static>(value: MyBox<T>) -> Element
+    where
+        T: Display,
+    {
+        rsx! {}
+    }
+
     #[derive(Props, Clone, PartialEq)]
     struct TakesCloneManualProps<T: Clone + PartialEq + 'static> {
         value: T,