convert T into signals automatically

This commit is contained in:
Evan Almloff committed 2024-01-29 13:36:39 -06:00
1 parent 1847c737e9
commit 50e3216d8b
14 files changed
+441 -103

No files matched your search

+255 -58
View File
@@ -8,10 +8,12 @@
use proc_macro2::TokenStream;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse::Error, PathArguments};
use quote::quote;
use syn::{parse_quote, Type};
pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let data = match &ast.data {
@@ -515,6 +517,7 @@ mod struct_info {
use syn::{Expr, Ident};
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::looks_like_signal_type;
use super::util::{
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple,
@@ -576,6 +579,89 @@ mod struct_info {
generics
}
fn has_signal_fields(&self) -> bool {
self.fields.iter().any(|f| looks_like_signal_type(f.ty))
}
fn memoize_impl(&self) -> Result<TokenStream, Error> {
// First check if there are any Signal fields, if there are not, we can just use the partialEq impl
let has_signal_fields = self.has_signal_fields();
if has_signal_fields {
let signal_fields: Vec<_> = self
.included_fields()
.filter(|f| looks_like_signal_type(f.ty))
.map(|f| {
let name = f.name;
quote!(#name)
})
.collect();
Ok(quote! {
// First check if the fields are equal
let exactly_equal = self == new;
if exactly_equal {
return true;
}
// If they are not, move over the signal fields and check if they are equal
#(
let mut #signal_fields = self.#signal_fields;
self.#signal_fields = new.#signal_fields;
)*
// Then check if the fields are equal again
let non_signal_fields_equal = self == new;
// If they are not equal, we still need to rerun the component
if !non_signal_fields_equal {
return false;
}
trait NonPartialEq: Sized {
fn compare(&self, other: &Self) -> bool;
}
impl<T> NonPartialEq for &&T {
fn compare(&self, other: &Self) -> bool {
false
}
}
trait CanPartialEq: PartialEq {
fn compare(&self, other: &Self) -> bool;
}
impl<T: PartialEq> CanPartialEq for T {
fn compare(&self, other: &Self) -> bool {
self == other
}
}
// If they are equal, we don't need to rerun the component we can just update the existing signals
#(
// Try to memo the signal
let field_eq = {
let old_value: &_ = &*#signal_fields.peek();
let new_value: &_ = &*new.#signal_fields.peek();
(&old_value).compare(&&new_value)
};
if !field_eq {
(#signal_fields).set(new.#signal_fields.take());
}
// Move the old value back
self.#signal_fields = #signal_fields;
)*
true
})
} else {
Ok(quote! {
self == new
})
}
}
pub fn builder_creation_impl(&self) -> Result<TokenStream, Error> {
let StructInfo {
ref vis,
@@ -584,12 +670,6 @@ mod struct_info {
..
} = *self;
// we're generating stuff that goes into unsafe code here
// we use the heuristic: are there *any* generic parameters?
// If so, then they might have non-static lifetimes and we can't compare two generic things that *might borrow*
// Therefore, we will generate code that shortcircuits the "comparison" in memoization
let are_there_generics = !self.generics.params.is_empty();
let generics = self.generics.clone();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let (_, b_initial_generics, _) = self.generics.split_for_impl();
@@ -669,20 +749,26 @@ Finally, call `.build()` to create the instance of `{name}`.
.extend(predicates.predicates.clone());
}
let can_memoize = match are_there_generics {
true => quote! { false },
false => quote! { self == other },
};
let memoize = self.memoize_impl()?;
let extend_fields = self.extend_fields().map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
});
let extend_fields_value = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: Vec::new())
});
let global_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
let ty = f.ty;
quote!(#name: #ty)
})
.chain(self.has_signal_fields().then(|| quote!(owner: Owner)));
let global_fields_value = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: Vec::new())
})
.chain(
self.has_signal_fields()
.then(|| quote!(owner: Owner::default())),
);
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
@@ -690,7 +776,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#[allow(dead_code, clippy::type_complexity)]
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
#(#extend_fields_value,)*
#(#global_fields_value,)*
fields: #empties_tuple,
_phantom: ::core::default::Default::default(),
}
@@ -701,7 +787,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#builder_type_doc
#[allow(dead_code, non_camel_case_types, non_snake_case)]
#vis struct #builder_name #b_generics {
#(#extend_fields,)*
#(#global_fields,)*
fields: #all_fields_param,
_phantom: (#( #phantom_generics ),*),
}
@@ -713,11 +799,10 @@ Finally, call `.build()` to create the instance of `{name}`.
fn builder() -> Self::Builder {
#name::builder()
}
fn memoize(&self, other: &Self) -> bool {
#can_memoize
fn memoize(&mut self, new: &Self) -> bool {
#memoize
}
}
})
}
@@ -966,22 +1051,29 @@ Finally, call `.build()` to create the instance of `{name}`.
let arg_type = field_type;
// If the field is auto_into, we need to add a generic parameter to the builder for specialization
let mut marker = None;
let (arg_type, arg_expr) =
if field.builder_attr.auto_into || field.builder_attr.strip_option {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
quote!(dioxus_core::prelude::SuperInto::super_into(#field_name)),
)
} else if field.builder_attr.from_displayable {
(
quote!(impl ::core::fmt::Display),
quote!(#field_name.to_string()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let (arg_type, arg_expr) = if looks_like_signal_type(arg_type) {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
// If this looks like a signal type, we automatically convert it with SuperInto and use the props struct as the owner
quote!(with_owner(self.owner.clone(), move || dioxus_core::prelude::SuperInto::super_into(#field_name))),
)
} else if field.builder_attr.auto_into || field.builder_attr.strip_option {
let marker_ident = syn::Ident::new("__Marker", proc_macro2::Span::call_site());
marker = Some(marker_ident.clone());
(
quote!(impl dioxus_core::prelude::SuperInto<#arg_type, #marker_ident>),
quote!(dioxus_core::prelude::SuperInto::super_into(#field_name)),
)
} else if field.builder_attr.from_displayable {
(
quote!(impl ::core::fmt::Display),
quote!(#field_name.to_string()),
)
} else {
(quote!(#arg_type), quote!(#field_name))
};
let repeated_fields_error_type_name = syn::Ident::new(
&format!(
@@ -993,10 +1085,13 @@ Finally, call `.build()` to create the instance of `{name}`.
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
quote!(#name: self.#name)
});
let forward_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
quote!(#name: self.#name)
})
.chain(self.has_signal_fields().then(|| quote!(owner: self.owner)));
Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
@@ -1007,7 +1102,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
#(#forward_extended_fields,)*
#(#forward_fields,)*
fields: ( #(#reconstructing,)* ),
_phantom: self._phantom,
}
@@ -1132,7 +1227,7 @@ Finally, call `.build()` to create the instance of `{name}`.
note = #early_build_error_message
)]
pub fn build(self, _: #early_build_error_type_name) -> #name #ty_generics {
panic!();
panic!()
}
}
})
@@ -1229,26 +1324,84 @@ Finally, call `.build()` to create the instance of `{name}`.
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
// correct is roughly impossible.
let doc =
format!("Finalise the builder and create its [`{name}`] instance");
format!("Finalize the builder and create its [`{name}`] instance");
quote!(#[doc = #doc])
}
}
} else {
quote!()
};
quote!(
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
#( #field_names ),*
if self.has_signal_fields() {
let name = Ident::new(&format!("{}WithOwner", name), name.span());
let original_name = &self.name;
quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[derive(Clone)]
struct #name #ty_generics {
inner: #original_name #ty_generics,
owner: Owner,
}
impl #impl_generics PartialEq for #name #ty_generics #where_clause {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl #impl_generics #name #ty_generics #where_clause {
/// Create a component from the props.
fn into_vcomponent<M: 'static>(
self,
render_fn: impl dioxus_core::prelude::ComponentFunction<#original_name #ty_generics, M>,
component_name: &'static str,
) -> dioxus_core::VComponent {
use dioxus_core::prelude::ComponentFunction;
dioxus_core::VComponent::new(move |wrapper: Self| render_fn.rebuild(wrapper.inner), self, component_name)
}
}
impl #impl_generics dioxus_core::prelude::Properties for #name #ty_generics #where_clause {
type Builder = ();
fn builder() -> Self::Builder {
unreachable!()
}
fn memoize(&mut self, new: &Self) -> bool {
self.inner.memoize(&new.inner)
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
inner: #original_name {
#( #field_names ),*
},
owner: self.owner,
}
}
}
}
)
} else {
quote!(
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name #modified_ty_generics #where_clause {
#doc
pub fn build(self) -> #name #ty_generics {
let ( #(#descructuring,)* ) = self.fields;
#( #assignments )*
#name {
#( #field_names ),*
}
}
}
)
}
}
}
@@ -1374,3 +1527,47 @@ Finally, call `.build()` to create the instance of `{name}`.
}
}
}
fn looks_like_signal_type(ty: &Type) -> bool {
match ty {
Type::Path(ty) => {
if ty.qself.is_some() {
return false;
}
let path = &ty.path;
let mut path_segments_without_generics = Vec::new();
let mut generic_arg_count = 0;
for segment in &path.segments {
let mut segment = segment.clone();
match segment.arguments {
PathArguments::AngleBracketed(_) => generic_arg_count += 1,
PathArguments::Parenthesized(_) => {
return false;
}
_ => {}
}
segment.arguments = syn::PathArguments::None;
path_segments_without_generics.push(segment);
}
// If there is more than the type and the send/sync generic, it doesn't look like our signal
if generic_arg_count > 2 {
return false;
}
let path_without_generics = syn::Path {
leading_colon: None,
segments: Punctuated::from_iter(path_segments_without_generics),
};
path_without_generics == parse_quote!(dioxus_core::prelude::Signal)
|| path_without_generics == parse_quote!(prelude::Signal)
|| path_without_generics == parse_quote!(Signal)
}
_ => false,
}
}