Преглед на файлове

Support Optional Read Signals (#2761)

* progress: add test

* feat: optional read signals

* revision: doc

* fix: fmt

* revision: imports

* fix: return type only if option

* add a few more compile tests for optional props

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Miles Murgaw преди 11 месеца
родител
ревизия
e0bb67b476
променени са 2 файла, в които са добавени 94 реда и са изтрити 21 реда
  1. 41 21
      packages/core-macro/src/props/mod.rs
  2. 53 0
      packages/core-macro/tests/rsx.rs

+ 41 - 21
packages/core-macro/src/props/mod.rs

@@ -13,7 +13,7 @@ use syn::spanned::Spanned;
 use syn::{parse::Error, PathArguments};
 
 use quote::quote;
-use syn::{parse_quote, Type};
+use syn::{parse_quote, GenericArgument, PathSegment, Type};
 
 pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
     let data = match &ast.data {
@@ -228,8 +228,7 @@ mod field_info {
 
                 // auto detect optional
                 let strip_option_auto = builder_attr.strip_option
-                    || !builder_attr.ignore_option
-                        && type_from_inside_option(&field.ty, true).is_some();
+                    || !builder_attr.ignore_option && type_from_inside_option(&field.ty).is_some();
                 if !builder_attr.strip_option && strip_option_auto {
                     builder_attr.strip_option = true;
                     builder_attr.default = Some(
@@ -482,31 +481,52 @@ mod field_info {
     }
 }
 
-fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> {
-    let path = if let syn::Type::Path(type_path) = ty {
-        if type_path.qself.is_some() {
-            return None;
-        } else {
-            &type_path.path
-        }
-    } else {
+fn type_from_inside_option(ty: &Type) -> Option<&Type> {
+    let Type::Path(type_path) = ty else {
         return None;
     };
-    let segment = path.segments.last()?;
-    if check_option_name && segment.ident != "Option" {
+
+    if type_path.qself.is_some() {
         return None;
     }
-    let generic_params =
-        if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
-            generic_params
-        } else {
+
+    let path = &type_path.path;
+    let seg = path.segments.last()?;
+
+    // If the segment is a supported optional type, provide the inner type.
+    if seg.ident == "Option" || seg.ident == "ReadOnlySignal" || seg.ident == "ReadSignal" {
+        // Get the inner type. E.g. the `u16` in `Option<u16>` or `Option` in `ReadSignal<Option<bool>>`
+        let inner_type = extract_inner_type_from_segment(seg)?;
+        let Type::Path(inner_path) = inner_type else {
             return None;
         };
-    if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
-        Some(ty)
-    } else {
-        None
+
+        // If we're entering an `Option`, we must get the innermost type. Otherwise, return the current type.
+        let inner_seg = inner_path.path.segments.last()?;
+        if inner_seg.ident == "Option" {
+            // Get the innermost type.
+            let innermost_type = extract_inner_type_from_segment(inner_seg)?;
+            return Some(innermost_type);
+        } else if seg.ident == "Option" {
+            // Return the inner type only if the parent is an `Option`.
+            return Some(inner_type);
+        }
     }
+
+    None
+}
+
+// Extract the inner type from a path segment.
+fn extract_inner_type_from_segment(segment: &PathSegment) -> Option<&Type> {
+    let PathArguments::AngleBracketed(generic_args) = &segment.arguments else {
+        return None;
+    };
+
+    let GenericArgument::Type(final_type) = generic_args.args.first()? else {
+        return None;
+    };
+
+    Some(final_type)
 }
 
 mod struct_info {

+ 53 - 0
packages/core-macro/tests/rsx.rs

@@ -51,3 +51,56 @@ mod test_default_into {
         pub some_other_data: bool,
     }
 }
+/// This test ensures that read-only signals that contain an option (`Signal<Option<u16>>`)
+/// are correctly created as default when not provided.
+///
+/// These are compile-time tests.
+/// See https://github.com/DioxusLabs/dioxus/issues/2648
+#[cfg(test)]
+#[allow(unused)]
+mod test_optional_signals {
+    use dioxus::prelude::*;
+
+    // Test if test components fail to compile.
+    #[component]
+    fn UsesComponents() -> Element {
+        rsx! {
+            PropsStruct {
+                regular_read_signal: ReadOnlySignal::new(Signal::new(1234)),
+            }
+            PropsStruct {
+                optional_read_signal: 1234,
+                regular_read_signal: 123u16,
+            }
+            PropParams {}
+            PropParams {
+                opt_read_sig: 1234
+            }
+            DoubleOption {}
+            DoubleOption { optional: Some(1234) }
+        }
+    }
+
+    // Test props as struct param.
+    #[derive(Props, Clone, PartialEq)]
+    struct MyTestProps {
+        pub optional_read_signal: ReadOnlySignal<Option<u16>>,
+        pub regular_read_signal: ReadOnlySignal<u16>,
+    }
+
+    #[component]
+    fn PropsStruct(props: MyTestProps) -> Element {
+        rsx! { "hi" }
+    }
+
+    // Test props as params.
+    #[component]
+    fn PropParams(opt_read_sig: ReadOnlySignal<Option<u16>>) -> Element {
+        rsx! { "hi!" }
+    }
+
+    #[component]
+    fn DoubleOption(optional: Option<Option<u16>>) -> Element {
+        rsx! { "hi!" }
+    }
+}