From 7b51bb806018453a5a3931dd8ae92582e63e2342 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 26 Sep 2023 19:23:00 -0500 Subject: [PATCH] WIP: add extends to the props macro --- examples/spread.rs | 69 +---- packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/props/mod.rs | 338 +++++++++++++++++------- packages/core/src/lib.rs | 7 +- packages/core/src/nodes.rs | 4 +- packages/html-internal-macro/src/lib.rs | 2 +- 6 files changed, 270 insertions(+), 151 deletions(-) diff --git a/examples/spread.rs b/examples/spread.rs index 75c026aff..f3be4d2b7 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,3 +1,4 @@ +use crate::dioxus_elements::ExtendedDivMarker; use dioxus::{ core::{exports::bumpalo::Bump, Attribute, HasAttributesBox}, html::{ExtendedGlobalAttributesMarker, GlobalAttributesExtension}, @@ -13,65 +14,15 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(::dioxus::core::LazyNodes::new( - move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { - name: "src/main.rs:15:15:289", - roots: &[::dioxus::core::TemplateNode::Dynamic { id: 0usize }], - node_paths: &[&[0u8]], - attr_paths: &[], - }; - ::dioxus::core::VNode { - parent: None, - key: None, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in( - 1usize, - __cx.bump(), - ) - .into(), - dynamic_nodes: __cx.bump().alloc([__cx.component( - Component, - Props { - bump: __cx.bump(), - attributes: Vec::new(), - } - .width(10) - .height("100px"), - "Component", - )]), - dynamic_attrs: __cx.bump().alloc([]), - } - }, - )) -} - -#[derive(Props)] -struct Props<'a> { - bump: &'a Bump, - attributes: Vec>, -} - -impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> { - fn push_attribute( - mut self, - name: &'a str, - ns: Option<&'static str>, - attr: impl IntoAttributeValue<'a>, - volatile: bool, - ) -> Self { - self.attributes.push(Attribute { - name, - namespace: ns, - value: attr.into_value(self.bump), - volatile, - }); - self + render! { + Component { + width: "10px", + height: "10px", + left: 1, + } } } -impl ExtendedGlobalAttributesMarker for Props<'_> {} - fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { let attributes = &*cx.props.attributes; render! { @@ -80,3 +31,9 @@ fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { } } } + +#[derive(Props)] +struct Props<'a> { + #[props(extends = GlobalAttributes)] + attributes: Vec>, +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index d928d5fec..c30f85289 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] } dioxus-rsx = { workspace = true } dioxus-core = { workspace = true } constcat = "0.3.0" +convert_case = "^0.6.0" # testing [dev-dependencies] diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f5ad99ce8..6285643c5 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .included_fields() .map(|f| struct_info.field_impl(f)) .collect::, _>>()?; + let extends = struct_info + .extend_fields() + .map(|f| struct_info.extends_impl(f)) + .collect::, _>>()?; let fields = quote!(#(#fields)*).into_iter(); let required_fields = struct_info .included_fields() @@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { #builder_creation #conversion_helper #( #fields )* + #( #extends )* #( #required_fields )* #build_method } @@ -167,8 +172,8 @@ mod field_info { use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; - use syn::Expr; use syn::{parse::Error, punctuated::Punctuated}; + use syn::{Expr, Path}; use super::util::{ expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, @@ -199,6 +204,13 @@ mod field_info { ); } + // extended field is automatically empty + if !builder_attr.extends.is_empty() { + builder_attr.default = Some( + syn::parse(quote!(::core::default::Default::default()).into()).unwrap(), + ); + } + // auto detect optional let strip_option_auto = builder_attr.strip_option || !builder_attr.ignore_option @@ -257,6 +269,7 @@ mod field_info { pub auto_into: bool, pub strip_option: bool, pub ignore_option: bool, + pub extends: Vec, } impl FieldBuilderAttr { @@ -309,6 +322,17 @@ mod field_info { let name = expr_to_single_string(&assign.left) .ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?; match name.as_str() { + "extends" => { + if let syn::Expr::Path(path) = *assign.right { + self.extends.push(path.path); + Ok(()) + } else { + Err(Error::new_spanned( + assign.right, + "Expected simple identifier", + )) + } + } "default" => { self.default = Some(*assign.right); Ok(()) @@ -363,6 +387,11 @@ mod field_info { Ok(()) } + "extend" => { + self.extends.push(path.path); + Ok(()) + } + _ => { macro_rules! handle_fields { ( $( $flag:expr, $field:ident, $already:expr; )* ) => { @@ -466,11 +495,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s } mod struct_info { + use convert_case::{Case, Casing}; use proc_macro2::TokenStream; use quote::quote; use syn::parse::Error; use syn::punctuated::Punctuated; - use syn::Expr; + use syn::spanned::Spanned; + use syn::visit::Visit; + use syn::{Expr, Ident}; use super::field_info::{FieldBuilderAttr, FieldInfo}; use super::util::{ @@ -493,7 +525,46 @@ mod struct_info { impl<'a> StructInfo<'a> { pub fn included_fields(&self) -> impl Iterator> { - self.fields.iter().filter(|f| !f.builder_attr.skip) + self.fields + .iter() + .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty()) + } + + pub fn extend_fields(&self) -> impl Iterator> { + self.fields + .iter() + .filter(|f| !f.builder_attr.extends.is_empty()) + } + + fn extend_lifetime(&self) -> syn::Result> { + let first_extend = self.extend_fields().next(); + + match first_extend { + Some(f) => { + struct VisitFirstLifetime(Option); + + impl Visit<'_> for VisitFirstLifetime { + fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) { + if self.0.is_none() { + self.0 = Some(lifetime.clone()); + } + } + } + + let name = f.name; + let mut visitor = VisitFirstLifetime(None); + + visitor.visit_type(&f.ty); + + visitor.0.ok_or_else(|| { + syn::Error::new_spanned( + name, + "Unable to find lifetime for extended field. Please specify it manually", + ) + }).map(Some) + } + None => Ok(None), + } } pub fn new( @@ -542,29 +613,12 @@ mod struct_info { let generics = self.generics.clone(); let (_, ty_generics, where_clause) = generics.split_for_impl(); - let impl_generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); - }); + let impl_generics = self.generics.clone(); let (impl_generics, b_initial_generics, _) = impl_generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), ); let b_generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); g.params.insert(0, all_fields_param.clone()); }); let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type())); @@ -629,8 +683,7 @@ Finally, call `.build()` to create the instance of `{name}`. quote!(#[doc(hidden)]) }; - let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) = - b_generics.split_for_impl(); + let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl(); let mut b_generics_where: syn::WhereClause = syn::parse2(quote! { where TypedBuilderFields: Clone })?; @@ -650,12 +703,26 @@ Finally, call `.build()` to create the instance of `{name}`. false => quote! { true }, }; + 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 extend_lifetime = self + .extend_lifetime()? + .unwrap_or(syn::Lifetime::new("'_", proc_macro2::Span::call_site())); + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] #[allow(dead_code)] - #vis fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { + #vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { #builder_name { + #(#extend_fields_value,)* bump: _cx.bump(), fields: #empties_tuple, _phantom: ::core::default::Default::default(), @@ -667,27 +734,18 @@ 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 { - bump: &'__bump ::dioxus::core::exports::bumpalo::Bump, + #(#extend_fields,)* + bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump, fields: #all_fields_param, _phantom: (#( #phantom_generics ),*), } - impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where { - fn clone(&self) -> Self { - Self { - bump: self.bump, - fields: self.fields.clone(), - _phantom: ::core::default::Default::default(), - } - } - } - - impl #impl_generics ::dioxus::prelude::Properties<'__bump> for #name #ty_generics + impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics #b_generics_where_extras_predicates { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder(_cx: &'__bump ::dioxus::prelude::ScopeState) -> Self::Builder { + fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder { #name::builder(_cx) } unsafe fn memoize(&self, other: &Self) -> bool { @@ -723,6 +781,137 @@ Finally, call `.build()` to create the instance of `{name}`. }) } + pub fn extends_impl(&self, field: &FieldInfo) -> Result { + let StructInfo { + ref builder_name, .. + } = *self; + + let field_name = field.name; + + let descructuring = self.included_fields().map(|f| { + if f.ordinal == field.ordinal { + quote!(_) + } else { + let name = f.name; + quote!(#name) + } + }); + let reconstructing = self.included_fields().map(|f| f.name); + + // Add the bump lifetime to the generics + let mut ty_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { + syn::GenericParam::Type(type_param) => { + let ident = type_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + syn::GenericParam::Lifetime(lifetime_def) => { + syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone()) + } + syn::GenericParam::Const(const_param) => { + let ident = const_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + }) + .collect(); + let mut target_generics_tuple = empty_type_tuple(); + let mut ty_generics_tuple = empty_type_tuple(); + let generics = self.modify_generics(|g| { + let index_after_lifetime_in_generics = g + .params + .iter() + .filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_))) + .count(); + for f in self.included_fields() { + if f.ordinal == field.ordinal { + ty_generics_tuple.elems.push_value(empty_type()); + target_generics_tuple + .elems + .push_value(f.tuplized_type_ty_param()); + } else { + g.params + .insert(index_after_lifetime_in_generics, f.generic_ty_param()); + let generic_argument: syn::Type = f.type_ident(); + ty_generics_tuple.elems.push_value(generic_argument.clone()); + target_generics_tuple.elems.push_value(generic_argument); + } + ty_generics_tuple.elems.push_punct(Default::default()); + target_generics_tuple.elems.push_punct(Default::default()); + } + }); + let mut target_generics = ty_generics.clone(); + let index_after_lifetime_in_generics = target_generics + .iter() + .filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_))) + .count(); + target_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(target_generics_tuple.into()), + ); + ty_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(ty_generics_tuple.into()), + ); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let forward_extended_fields = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: self.#name) + }); + + let extends_impl = field.builder_attr.extends.iter().map(|path| { + let name_str = path_to_single_string(path).unwrap(); + let camel_name = name_str.to_case(Case::UpperCamel); + let marker_name = Ident::new( + format!("Extended{}Marker", &camel_name).as_str(), + path.span(), + ); + quote! { + impl #impl_generics #marker_name for #builder_name < #( #ty_generics ),* > #where_clause {} + } + }); + let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned( + field_name, + "Unable to find lifetime for extended field. Please specify it manually", + ))?; + + Ok(quote! { + impl #impl_generics ::dioxus::prelude::HasAttributesBox<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { + fn push_attribute( + mut self, + name: &#extend_lifetime str, + ns: Option<&'static str>, + attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>, + volatile: bool + ) -> Self { + let ( #(#descructuring,)* ) = self.fields; + self.#field_name.push( + ::dioxus::core::Attribute::new( + name, + { + use ::dioxus::prelude::IntoAttributeValue; + attr.into_value(self.bump) + }, + ns, + volatile, + ) + ); + #builder_name { + #(#forward_extended_fields,)* + bump: self.bump, + fields: ( #(#reconstructing,)* ), + _phantom: self._phantom, + } + } + } + + #(#extends_impl)* + }) + } + pub fn field_impl(&self, field: &FieldInfo) -> Result { let StructInfo { ref builder_name, .. @@ -744,12 +933,11 @@ Finally, call `.build()` to create the instance of `{name}`. .. } = field; // Add the bump lifetime to the generics - let mut ty_generics: Vec = vec![syn::GenericArgument::Lifetime( - syn::Lifetime::new("'__bump", proc_macro2::Span::call_site()), - )]; - - ty_generics.extend(self.generics.params.iter().map( - |generic_param| match generic_param { + let mut ty_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { syn::GenericParam::Type(type_param) => { let ident = type_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() @@ -761,19 +949,11 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = const_param.ident.clone(); syn::parse(quote!(#ident).into()).unwrap() } - }, - )); + }) + .collect(); let mut target_generics_tuple = empty_type_tuple(); let mut ty_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); let index_after_lifetime_in_generics = g .params .iter() @@ -851,6 +1031,11 @@ 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) + }); + Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause { @@ -859,6 +1044,7 @@ Finally, call `.build()` to create the instance of `{name}`. let #field_name = (#arg_expr,); let ( #(#descructuring,)* ) = self.fields; #builder_name { + #(#forward_extended_fields,)* bump: self.bump, fields: ( #(#reconstructing,)* ), _phantom: self._phantom, @@ -893,13 +1079,11 @@ Finally, call `.build()` to create the instance of `{name}`. .. } = field; // Add a bump lifetime to the generics - let mut builder_generics: Vec = - vec![syn::GenericArgument::Lifetime(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))]; - builder_generics.extend(self.generics.params.iter().map(|generic_param| { - match generic_param { + let mut builder_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { syn::GenericParam::Type(type_param) => { let ident = &type_param.ident; syn::parse(quote!(#ident).into()).unwrap() @@ -911,18 +1095,10 @@ Finally, call `.build()` to create the instance of `{name}`. let ident = &const_param.ident; syn::parse(quote!(#ident).into()).unwrap() } - } - })); + }) + .collect(); let mut builder_generics_tuple = empty_type_tuple(); let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); let index_after_lifetime_in_generics = g .params .iter() @@ -1007,15 +1183,6 @@ Finally, call `.build()` to create the instance of `{name}`. } = *self; let generics = self.modify_generics(|g| { - // Add a bump lifetime to the generics - g.params.insert( - 0, - syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - ))), - ); - let index_after_lifetime_in_generics = g .params .iter() @@ -1054,15 +1221,6 @@ Finally, call `.build()` to create the instance of `{name}`. let (_, ty_generics, where_clause) = self.generics.split_for_impl(); let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| { - // Add a bump lifetime to the generics - args.insert( - 0, - syn::GenericArgument::Lifetime(syn::Lifetime::new( - "'__bump", - proc_macro2::Span::call_site(), - )), - ); - args.insert( 0, syn::GenericArgument::Type( @@ -1088,7 +1246,9 @@ Finally, call `.build()` to create the instance of `{name}`. // reordering based on that, but for now this much simpler thing is a reasonable approach. let assignments = self.fields.iter().map(|field| { let name = &field.name; - if let Some(ref default) = field.builder_attr.default { + if !field.builder_attr.extends.is_empty() { + quote!(let #name = self.#name;) + } else if let Some(ref default) = field.builder_attr.default { if field.builder_attr.skip { quote!(let #name = #default;) } else { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dbebd33e0..5723e9418 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -89,9 +89,10 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, provide_context, provide_context_to_scope, provide_root_context, push_future, remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue, - AttributeType, Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, - LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, - Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + AttributeType, Component, Element, Event, EventHandler, Fragment, HasAttributesBox, + IntoAttributeValue, LazyNodes, MountedAttribute, Properties, Runtime, RuntimeGuard, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, + VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a31481195..d0d10062a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -899,12 +899,12 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } -pub trait HasAttributesBox<'a, T> { +pub trait HasAttributesBox<'a> { fn push_attribute( self, name: &'a str, ns: Option<&'static str>, attr: impl IntoAttributeValue<'a>, volatile: bool, - ) -> T; + ) -> Self; } diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 4b7d23ad5..99c3db0d1 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/packages/html-internal-macro/src/lib.rs @@ -78,7 +78,7 @@ impl ToTokens for ImplExtensionAttributes { pub trait #extension_name<'a> { #(#defs)* } - impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a, T> + #marker_name { + impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a> + #marker_name { #(#impls)* } });