Allow spread attributes to be set directly (#3881)

* allow spread attributes to be set directly

* Remove ordering requirement
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-04-07 16:35:16 -07:00
1 parent 8c866b783d
commit 589440b9cb
3 files changed
+119 -42

No files matched your search

+56 -42
View File
@@ -33,7 +33,9 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let fields = quote!(#(#fields)*).into_iter();
let required_fields = struct_info
.included_fields()
.filter(|f| f.builder_attr.default.is_none())
.filter(|f| {
f.builder_attr.default.is_none() && f.builder_attr.extends.is_empty()
})
.map(|f| struct_info.required_field_impl(f))
.collect::<Result<Vec<_>, _>>()?;
let build_method = struct_info.build_method_impl();
@@ -172,7 +174,7 @@ mod util {
mod field_info {
use crate::props::type_from_inside_option;
use proc_macro2::TokenStream;
use quote::quote;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{parse::Error, punctuated::Punctuated};
use syn::{parse_quote, Expr, Path};
@@ -269,6 +271,13 @@ mod field_info {
}
.into()
}
pub fn extends_vec_ident(&self) -> Option<syn::Ident> {
(!self.builder_attr.extends.is_empty()).then(|| {
let ident = &self.name;
format_ident!("__{ident}_attributes")
})
}
}
#[derive(Debug, Default, Clone)]
@@ -566,9 +575,7 @@ mod struct_info {
impl<'a> StructInfo<'a> {
pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
self.fields
.iter()
.filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
self.fields.iter().filter(|f| !f.builder_attr.skip)
}
pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
@@ -670,7 +677,6 @@ mod struct_info {
let regular_fields: Vec<_> = self
.included_fields()
.chain(self.extend_fields())
.filter(|f| !looks_like_signal_type(f.ty) && !looks_like_callback_type(f.ty))
.map(|f| {
let name = f.name;
@@ -840,7 +846,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let global_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
let name = f.extends_vec_ident();
let ty = f.ty;
quote!(#name: #ty)
})
@@ -848,7 +854,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let global_fields_value = self
.extend_fields()
.map(|f| {
let name = f.name;
let name = f.extends_vec_ident();
quote!(#name: Vec::new())
})
.chain(
@@ -922,15 +928,11 @@ Finally, call `.build()` to create the instance of `{name}`.
ref builder_name, ..
} = *self;
let field_name = field.name;
let field_name = field.extends_vec_ident().unwrap();
let descructuring = self.included_fields().map(|f| {
if f.ordinal == field.ordinal {
quote!(_)
} else {
let name = f.name;
quote!(#name)
}
let name = f.name;
quote!(#name)
});
let reconstructing = self.included_fields().map(|f| f.name);
@@ -962,7 +964,12 @@ Finally, call `.build()` to create the instance of `{name}`.
.count();
for f in self.included_fields() {
if f.ordinal == field.ordinal {
ty_generics_tuple.elems.push_value(empty_type());
g.params.insert(
index_after_lifetime_in_generics,
syn::GenericParam::Type(self.generic_builder_param(f)),
);
let generic_argument: syn::Type = f.type_ident();
ty_generics_tuple.elems.push_value(generic_argument.clone());
target_generics_tuple
.elems
.push_value(f.tuplized_type_ty_param());
@@ -993,7 +1000,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let (impl_generics, _, where_clause) = generics.split_for_impl();
let forward_extended_fields = self.extend_fields().map(|f| {
let name = f.name;
let name = f.extends_vec_ident();
quote!(#name: self.#name)
});
@@ -1176,7 +1183,7 @@ Finally, call `.build()` to create the instance of `{name}`.
let forward_fields = self
.extend_fields()
.map(|f| {
let name = f.name;
let name = f.extends_vec_ident();
quote!(#name: self.#name)
})
.chain(
@@ -1323,6 +1330,31 @@ Finally, call `.build()` to create the instance of `{name}`.
})
}
fn generic_builder_param(&self, field: &FieldInfo) -> syn::TypeParam {
let trait_ref = syn::TraitBound {
paren_token: None,
lifetimes: None,
modifier: syn::TraitBoundModifier::None,
path: syn::PathSegment {
ident: self.conversion_helper_trait_name.clone(),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: make_punctuated_single(syn::GenericArgument::Type(
field.ty.clone(),
)),
gt_token: Default::default(),
},
),
}
.into(),
};
let mut generic_param: syn::TypeParam = field.generic_ident.clone().into();
generic_param.bounds.push(trait_ref.into());
generic_param
}
pub fn build_method_impl(&self) -> TokenStream {
let StructInfo {
ref name,
@@ -1338,27 +1370,7 @@ Finally, call `.build()` to create the instance of `{name}`.
.count();
for field in self.included_fields() {
if field.builder_attr.default.is_some() {
let trait_ref = syn::TraitBound {
paren_token: None,
lifetimes: None,
modifier: syn::TraitBoundModifier::None,
path: syn::PathSegment {
ident: self.conversion_helper_trait_name.clone(),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: Default::default(),
args: make_punctuated_single(syn::GenericArgument::Type(
field.ty.clone(),
)),
gt_token: Default::default(),
},
),
}
.into(),
};
let mut generic_param: syn::TypeParam = field.generic_ident.clone().into();
generic_param.bounds.push(trait_ref.into());
let generic_param = self.generic_builder_param(field);
g.params
.insert(index_after_lifetime_in_generics, generic_param.into());
}
@@ -1395,10 +1407,12 @@ 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 !field.builder_attr.extends.is_empty() {
quote!(let #name = self.#name;)
if let Some(extends_vec) = field.extends_vec_ident() {
quote!{
let mut #name = #helper_trait_name::into_value(#name, || ::core::default::Default::default());
#name.extend(self.#extends_vec);
}
} else if let Some(ref default) = field.builder_attr.default {
// If field has `into`, apply it to the default value.
// Ignore any blank defaults as it causes type inference errors.
let is_default = *default == parse_quote!(::core::default::Default::default());
+8
View File
@@ -2,6 +2,14 @@ use super::*;
use crate::document;
use dioxus_html as dioxus_elements;
#[non_exhaustive]
#[derive(Clone, Props, PartialEq)]
pub struct OtherLinkProps {
pub rel: String,
#[props(extends = link, extends = GlobalAttributes)]
pub additional_attributes: Vec<Attribute>,
}
#[non_exhaustive]
#[derive(Clone, Props, PartialEq)]
pub struct LinkProps {
+55
View File
@@ -0,0 +1,55 @@
use dioxus::prelude::*;
// Regression test for https://github.com/DioxusLabs/dioxus/issues/3844
#[test]
fn forward_spreads() {
#[derive(Props, Clone, PartialEq)]
struct Comp1Props {
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
}
#[component]
fn Comp1(props: Comp1Props) -> Element {
rsx! {
Comp2 {
attributes: props.attributes.clone(),
height: "100%",
}
Comp2 {
height: "100%",
attributes: props.attributes.clone(),
}
}
}
#[derive(Props, Clone, PartialEq)]
struct CompProps2 {
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
}
#[component]
fn Comp2(props: CompProps2) -> Element {
let attributes = props.attributes;
rsx! {
div {
..attributes
}
}
}
let merged = || {
rsx! {
Comp1 {
width: "100%"
}
}
};
let dom = VirtualDom::prebuilt(merged);
let html = dioxus_ssr::render(&dom);
assert_eq!(
html,
r#"<div style="width:100%;height:100%;"></div><div style="width:100%;height:100%;"></div>"#
);
}