From 81efe92ad75ed6a962d2801f82d997ceaa81455f Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Sun, 1 Mar 2026 07:23:21 +0900 Subject: [PATCH] fix: allow bare None for Option props in html! macro The html! macro now detects bare `None` and bypasses IntoPropValue, avoiding type inference ambiguity when multiple Option -> Option impls exist. Fixes #3747. --- packages/yew-macro/src/derive_props/field.rs | 52 ++++++++++++- packages/yew-macro/src/props/component.rs | 35 ++++++++- .../src/html/conversion/into_prop_value.rs | 76 +++++++++++++++++++ 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/packages/yew-macro/src/derive_props/field.rs b/packages/yew-macro/src/derive_props/field.rs index 09ee9809048..6898acc89cf 100644 --- a/packages/yew-macro/src/derive_props/field.rs +++ b/packages/yew-macro/src/derive_props/field.rs @@ -5,11 +5,28 @@ use proc_macro2::{Ident, Span}; use quote::{format_ident, quote, quote_spanned}; use syn::parse::Result; use syn::spanned::Spanned; -use syn::{parse_quote, Attribute, Error, Expr, Field, GenericParam, Generics, Type, Visibility}; +use syn::{ + parse_quote, Attribute, Error, Expr, Field, GenericArgument, GenericParam, Generics, + PathArguments, Type, Visibility, +}; use super::should_preserve_attr; use crate::derive_props::generics::push_type_param; +fn is_option_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Option" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + return args.args.len() == 1 + && matches!(args.args.first(), Some(GenericArgument::Type(_))); + } + } + } + } + false +} + #[allow(clippy::large_enum_variant)] #[derive(PartialEq, Eq)] pub enum PropAttr { @@ -130,9 +147,24 @@ impl PropField { ) -> proc_macro2::TokenStream { let Self { name, ty, attr, .. } = self; let token_ty = Ident::new("__YewTokenTy", Span::mixed_site()); + let none_fn_name = format_ident!("{}_none", name, span = Span::mixed_site()); let build_fn = match attr { PropAttr::Required { wrapped_name } => { let check_struct = self.to_check_name(props_name); + let none_setter = if is_option_type(ty) { + quote! { + #[doc(hidden)] + #vis fn #none_fn_name<#token_ty>( + &mut self, + token: #token_ty, + ) -> #check_struct< #token_ty > { + self.wrapped.#wrapped_name = ::std::option::Option::Some(::std::option::Option::None); + #check_struct ( ::std::marker::PhantomData ) + } + } + } else { + quote! {} + }; quote! { #[doc(hidden)] #vis fn #name<#token_ty>( @@ -143,9 +175,25 @@ impl PropField { self.wrapped.#wrapped_name = ::std::option::Option::Some(value.into_prop_value()); #check_struct ( ::std::marker::PhantomData ) } + + #none_setter } } _ => { + let none_setter = if is_option_type(ty) { + quote! { + #[doc(hidden)] + #vis fn #none_fn_name<#token_ty>( + &mut self, + token: #token_ty, + ) -> #token_ty { + self.wrapped.#name = ::std::option::Option::Some(::std::option::Option::None); + token + } + } + } else { + quote! {} + }; quote! { #[doc(hidden)] #vis fn #name<#token_ty>( @@ -156,6 +204,8 @@ impl PropField { self.wrapped.#name = ::std::option::Option::Some(value.into_prop_value()); token } + + #none_setter } } }; diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs index f2eed2a5955..ea401732ba4 100644 --- a/packages/yew-macro/src/props/component.rs +++ b/packages/yew-macro/src/props/component.rs @@ -9,6 +9,17 @@ use syn::Expr; use super::{Prop, Props, SpecialProps, CHILDREN_LABEL}; +fn is_none_expr(expr: &Expr) -> bool { + matches!( + expr, + Expr::Path(syn::ExprPath { + attrs, + qself: None, + path, + }) if attrs.is_empty() && path.is_ident("None") + ) +} + struct BaseExpr { pub dot_dot: DotDot, pub expr: Expr, @@ -100,8 +111,18 @@ impl ComponentProps { let #token_ident = ::yew::html::AssertAllProps; }; let set_props = self.props.iter().map(|Prop { label, value, .. }| { - quote_spanned! {value.span()=> - let #token_ident = #builder_ident.#label(#token_ident, #value); + if is_none_expr(value) { + let none_setter = Ident::new( + &format!("{}_none", label), + label.span().resolved_at(Span::mixed_site()), + ); + quote_spanned! {value.span()=> + let #token_ident = #builder_ident.#none_setter(#token_ident); + } + } else { + quote_spanned! {value.span()=> + let #token_ident = #builder_ident.#label(#token_ident, #value); + } } }); let set_children = children_renderer.map(|children| { @@ -125,8 +146,14 @@ impl ComponentProps { Some(expr) => { let ident = Ident::new("__yew_props", props_ty.span()); let set_props = self.props.iter().map(|Prop { label, value, .. }| { - quote_spanned! {value.span().resolved_at(Span::call_site())=> - #ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value); + if is_none_expr(value) { + quote_spanned! {value.span().resolved_at(Span::call_site())=> + #ident.#label = ::std::option::Option::None; + } + } else { + quote_spanned! {value.span().resolved_at(Span::call_site())=> + #ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value); + } } }); let set_children = children_renderer.map(|children| { diff --git a/packages/yew/src/html/conversion/into_prop_value.rs b/packages/yew/src/html/conversion/into_prop_value.rs index 343ef2a28d4..05f8a99bcb4 100644 --- a/packages/yew/src/html/conversion/into_prop_value.rs +++ b/packages/yew/src/html/conversion/into_prop_value.rs @@ -570,4 +570,80 @@ mod test { let _ = html! { {&attr_value} }; } } + + #[test] + fn test_bare_none_option_string_prop() { + use crate::prelude::*; + + #[derive(PartialEq, Properties)] + pub struct Props { + pub foo: Option, + } + + #[component] + fn Comp(_props: &Props) -> Html { + html! {} + } + + let _ = html! { }; + let _ = html! { }; + let _ = html! { }; + } + + #[test] + fn test_bare_none_option_attr_value_prop() { + use crate::prelude::*; + + #[derive(PartialEq, Properties)] + pub struct Props { + pub foo: Option, + } + + #[component] + fn Comp(_props: &Props) -> Html { + html! {} + } + + let _ = html! { }; + let _ = html! { }; + let _ = html! { }; + } + + #[test] + fn test_bare_none_option_html_prop() { + use crate::prelude::*; + + #[derive(PartialEq, Properties)] + pub struct Props { + pub title: Option, + } + + #[component] + fn Comp(_props: &Props) -> Html { + html! {} + } + + let _ = html! { }; + let _ = html! { ::None} /> }; + } + + #[test] + fn test_bare_none_optional_prop_with_default() { + use crate::prelude::*; + + #[derive(PartialEq, Properties)] + pub struct Props { + #[prop_or_default] + pub foo: Option, + } + + #[component] + fn Comp(_props: &Props) -> Html { + html! {} + } + + let _ = html! { }; + let _ = html! { }; + let _ = html! { }; + } }