diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index ed89b7f5a2..fbcc5a99ce 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -8,10 +8,11 @@ use leptos_hot_reload::parsing::value_to_string; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt}; use syn::{ - parse::Parse, parse_quote, spanned::Spanned, - AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item, - ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, - Signature, Stmt, Type, TypePath, Visibility, + parse::Parse, parse_quote, spanned::Spanned, token::Colon, + visit_mut::VisitMut, AngleBracketedGenericArguments, Attribute, FnArg, + GenericArgument, GenericParam, Item, ItemFn, LitStr, Meta, Pat, PatIdent, + Path, PathArguments, ReturnType, Signature, Stmt, Type, TypeImplTrait, + TypeParam, TypePath, Visibility, }; pub struct Model { @@ -28,6 +29,7 @@ pub struct Model { impl Parse for Model { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut item = ItemFn::parse(input)?; + convert_impl_trait_to_generic(&mut item.sig); let docs = Docs::new(&item.attrs); @@ -1229,3 +1231,57 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool { pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident { Ident::new(&format!("__{ident}"), ident.span()) } + +/// Converts all `impl Trait`s in a function signature to use generic params instead. +pub fn convert_impl_trait_to_generic(sig: &mut Signature) { + fn new_generic_ident(i: usize, span: Span) -> Ident { + Ident::new(&format!("__ImplTrait{}", i), span) + } + + // First: visit all `impl Trait`s and replace them with new generic params. + #[derive(Default)] + struct RemoveImplTrait(Vec); + impl VisitMut for RemoveImplTrait { + fn visit_type_mut(&mut self, ty: &mut Type) { + syn::visit_mut::visit_type_mut(self, ty); + if matches!(ty, Type::ImplTrait(_)) { + let ident = new_generic_ident(self.0.len(), ty.span()); + let generic_type = Type::Path(TypePath { + qself: None, + path: Path::from(ident), + }); + let Type::ImplTrait(impl_trait) = + std::mem::replace(ty, generic_type) + else { + unreachable!(); + }; + self.0.push(impl_trait); + } + } + + // Early exits. + fn visit_attribute_mut(&mut self, _: &mut Attribute) {} + fn visit_pat_mut(&mut self, _: &mut Pat) {} + } + let mut visitor = RemoveImplTrait::default(); + for fn_arg in sig.inputs.iter_mut() { + visitor.visit_fn_arg_mut(fn_arg); + } + let RemoveImplTrait(impl_traits) = visitor; + + // Second: Add the new generic params into the signature. + for (i, impl_trait) in impl_traits.into_iter().enumerate() { + let span = impl_trait.span(); + let ident = new_generic_ident(i, span); + // We can simply append to the end (only lifetime params must be first). + // Note currently default generics are not allowed in `fn`, so not a concern. + sig.generics.params.push(GenericParam::Type(TypeParam { + attrs: vec![], + ident, + colon_token: Some(Colon { spans: [span] }), + bounds: impl_trait.bounds, + eq_token: None, + default: None, + })); + } +} diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 3079d7eb6f..b9e523f91b 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -37,7 +37,9 @@ impl Default for Mode { mod params; mod view; -use crate::component::unmodified_fn_name_from_fn_name; +use component::{ + convert_impl_trait_to_generic, unmodified_fn_name_from_fn_name, +}; use view::{client_template::render_template, render_view}; mod component; mod slice; @@ -608,6 +610,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { let Ok(mut dummy) = syn::parse::(s.clone()) else { return s; }; + convert_impl_trait_to_generic(&mut dummy.sig); + let parse_result = syn::parse::(s); if let (ref mut unexpanded, Ok(model)) = (&mut dummy, parse_result) { diff --git a/leptos_macro/tests/component.rs b/leptos_macro/tests/component.rs index f156507c06..305bba3cd0 100644 --- a/leptos_macro/tests/component.rs +++ b/leptos_macro/tests/component.rs @@ -8,20 +8,27 @@ fn Component( #[prop(strip_option)] strip_option: Option, #[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize, #[prop(into)] into: String, + impl_trait: impl Fn() -> i32 + 'static, ) -> impl IntoView { _ = optional; _ = optional_no_strip; _ = strip_option; _ = default; _ = into; + _ = impl_trait; } #[test] fn component() { - let cp = ComponentProps::builder().into("").strip_option(9).build(); + let cp = ComponentProps::builder() + .into("") + .strip_option(9) + .impl_trait(|| 42) + .build(); assert!(!cp.optional); assert_eq!(cp.optional_no_strip, None); assert_eq!(cp.strip_option, Some(9)); assert_eq!(cp.default, NonZeroUsize::new(10).unwrap()); assert_eq!(cp.into, ""); + assert_eq!((cp.impl_trait)(), 42); }