Skip to main content

tessera_macros/
lib.rs

1//! # Tessera Macros
2//!
3//! This crate provides procedural macros for the Tessera UI framework.
4//! The main export is the `#[tessera]` attribute macro, which transforms
5//! regular Rust functions into Tessera UI components.
6#![deny(
7    missing_docs,
8    clippy::unwrap_used,
9    rustdoc::broken_intra_doc_links,
10    rustdoc::invalid_rust_codeblocks,
11    rustdoc::invalid_html_tags
12)]
13
14use proc_macro::TokenStream;
15use quote::{format_ident, quote};
16use syn::{
17    Block, Expr, FnArg, GenericArgument, GenericParam, Generics, Ident, ItemFn, Pat, Path,
18    PathArguments, Token, Type, parse::Parse, parse_macro_input, parse_quote, visit_mut::VisitMut,
19};
20
21fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Result<syn::Path> {
22    if attr.is_empty() {
23        Ok(syn::parse_quote!(::tessera_ui))
24    } else {
25        let tokens: proc_macro2::TokenStream = attr.clone().into();
26        syn::parse(attr).map_err(|_| {
27            syn::Error::new_spanned(tokens, "expected a crate path like `crate` or `tessera_ui`")
28        })
29    }
30}
31
32struct ComponentAttr {
33    crate_path: Path,
34    layout: bool,
35}
36
37impl Parse for ComponentAttr {
38    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
39        if input.is_empty() {
40            return Ok(Self {
41                crate_path: parse_quote!(::tessera_ui),
42                layout: false,
43            });
44        }
45        let crate_path: Path = input.parse()?;
46        let layout = if input.is_empty() {
47            false
48        } else {
49            input.parse::<Token![,]>()?;
50            let role: Ident = input.parse()?;
51            if role != "layout" {
52                return Err(syn::Error::new_spanned(role, "expected `layout`"));
53            }
54            true
55        };
56        Ok(Self { crate_path, layout })
57    }
58}
59
60#[derive(Clone, Copy, Debug, Default)]
61struct PropAttrConfig {
62    into: bool,
63    skip_setter: bool,
64    skip_eq: bool,
65}
66/// The builder-helper shape a component parameter gets from its declared type.
67///
68/// The parameter type is the single source of truth: functor parameters carry
69/// their parsed functor signature so the generated setters never have to
70/// re-inspect the type.
71#[derive(Clone, Debug)]
72enum PropHelperKind {
73    Callback,
74    CallbackWith { arg: Type, ret: Type },
75    RenderSlot,
76    RenderSlotWith { arg: Type },
77}
78
79fn parse_prop_attr(attrs: &[syn::Attribute]) -> syn::Result<PropAttrConfig> {
80    let mut config = PropAttrConfig::default();
81    for attr in attrs {
82        if !attr.path().is_ident("prop") {
83            continue;
84        }
85
86        match &attr.meta {
87            syn::Meta::Path(_) => {
88                return Err(syn::Error::new_spanned(
89                    attr,
90                    "expected `#[prop(...)]`; supported options are `into`, `skip_setter`, and `skip_eq`",
91                ));
92            }
93            syn::Meta::List(_) => {
94                attr.parse_nested_meta(|meta| {
95                    if meta.path.is_ident("into") {
96                        if config.into {
97                            return Err(meta.error("duplicate `into` in #[prop(...)]"));
98                        }
99                        config.into = true;
100                        return Ok(());
101                    }
102                    if meta.path.is_ident("skip_setter") {
103                        if config.skip_setter {
104                            return Err(meta.error("duplicate `skip_setter` in #[prop(...)]"));
105                        }
106                        config.skip_setter = true;
107                        return Ok(());
108                    }
109                    if meta.path.is_ident("skip_eq") {
110                        if config.skip_eq {
111                            return Err(meta.error("duplicate `skip_eq` in #[prop(...)]"));
112                        }
113                        config.skip_eq = true;
114                        return Ok(());
115                    }
116                    if meta.path.is_ident("crate_path") {
117                        return Err(meta.error(
118                            "`crate_path` is only valid on the `#[tessera]` attribute, not on component parameters",
119                        ));
120                    }
121
122                    Err(meta.error(
123                        "unsupported #[prop(...)] option; supported options are `into`, `skip_setter`, and `skip_eq`",
124                    ))
125                })?;
126            }
127            syn::Meta::NameValue(_) => {
128                return Err(syn::Error::new_spanned(
129                    attr,
130                    "unsupported `#[prop = ...]` form; use `#[prop(...)]`",
131                ));
132            }
133        }
134    }
135    Ok(config)
136}
137
138fn option_inner_type(ty: &Type) -> Option<Type> {
139    let Type::Path(type_path) = ty else {
140        return None;
141    };
142    let segment = type_path.path.segments.last()?;
143    if segment.ident != "Option" {
144        return None;
145    }
146    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
147        return None;
148    };
149    arguments.args.iter().find_map(|arg| match arg {
150        GenericArgument::Type(inner) => Some(inner.clone()),
151        _ => None,
152    })
153}
154
155fn parse_functor_signature(ty: &Type, type_name: &str) -> Option<(Type, Type)> {
156    let Type::Path(type_path) = ty else {
157        return None;
158    };
159    let segment = type_path.path.segments.last()?;
160    if segment.ident != type_name {
161        return None;
162    }
163    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
164        return None;
165    };
166    let mut types = arguments.args.iter().filter_map(|arg| match arg {
167        GenericArgument::Type(ty) => Some(ty.clone()),
168        _ => None,
169    });
170    let arg = types.next()?;
171    let ret = types.next().unwrap_or_else(|| parse_quote!(()));
172    Some((arg, ret))
173}
174
175fn is_unit_type(ty: &Type) -> bool {
176    matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
177}
178
179fn type_last_segment_ident(ty: &Type) -> Option<&Ident> {
180    let Type::Path(type_path) = ty else {
181        return None;
182    };
183    type_path.path.segments.last().map(|segment| &segment.ident)
184}
185
186fn infer_prop_helper_kind(ty: &Type) -> Option<PropHelperKind> {
187    let value_ty = option_inner_type(ty).unwrap_or_else(|| ty.clone());
188    let ident = type_last_segment_ident(&value_ty)?;
189    if ident == "Callback" {
190        return Some(PropHelperKind::Callback);
191    }
192    if ident == "CallbackWith" {
193        let (arg, ret) = parse_functor_signature(&value_ty, "CallbackWith")?;
194        return Some(PropHelperKind::CallbackWith { arg, ret });
195    }
196    if ident == "RenderSlot" {
197        return Some(PropHelperKind::RenderSlot);
198    }
199    if ident == "RenderSlotWith" {
200        let (arg, _ret) = parse_functor_signature(&value_ty, "RenderSlotWith")?;
201        return Some(PropHelperKind::RenderSlotWith { arg });
202    }
203    None
204}
205
206fn is_arc_type(ty: &Type) -> bool {
207    type_last_segment_ident(ty).is_some_and(|ident| ident == "Arc")
208}
209
210fn is_rc_type(ty: &Type) -> bool {
211    type_last_segment_ident(ty).is_some_and(|ident| ident == "Rc")
212}
213
214fn field_compare_expr(field_ident: &Ident, ty: &Type) -> proc_macro2::TokenStream {
215    if is_arc_type(ty) {
216        return quote! { ::std::sync::Arc::ptr_eq(&self.#field_ident, &other.#field_ident) };
217    }
218    if is_rc_type(ty) {
219        return quote! { ::std::rc::Rc::ptr_eq(&self.#field_ident, &other.#field_ident) };
220    }
221    if let Some(inner_ty) = option_inner_type(ty) {
222        if is_arc_type(&inner_ty) {
223            return quote! {
224                match (&self.#field_ident, &other.#field_ident) {
225                    (Some(lhs), Some(rhs)) => ::std::sync::Arc::ptr_eq(lhs, rhs),
226                    (None, None) => true,
227                    _ => false,
228                }
229            };
230        }
231        if is_rc_type(&inner_ty) {
232            return quote! {
233                match (&self.#field_ident, &other.#field_ident) {
234                    (Some(lhs), Some(rhs)) => ::std::rc::Rc::ptr_eq(lhs, rhs),
235                    (None, None) => true,
236                    _ => false,
237                }
238            };
239        }
240    }
241    quote! { self.#field_ident == other.#field_ident }
242}
243
244#[cfg(feature = "shard")]
245#[derive(Default)]
246struct ShardMacroArgs {
247    crate_path: Option<Path>,
248    shard_crate_path: Option<Path>,
249    state_type: Option<Type>,
250    lifecycle: Option<Ident>,
251}
252
253#[cfg(feature = "shard")]
254struct ShardParam {
255    ident: Ident,
256    ty: Type,
257    is_router: bool,
258}
259
260#[cfg(feature = "shard")]
261fn parse_shard_params(sig: &syn::Signature) -> syn::Result<Vec<ShardParam>> {
262    let mut params = Vec::with_capacity(sig.inputs.len());
263    for arg in &sig.inputs {
264        match arg {
265            FnArg::Receiver(receiver) => {
266                return Err(syn::Error::new_spanned(
267                    receiver,
268                    "#[shard] does not support methods; use a free function",
269                ));
270            }
271            FnArg::Typed(pat_type) => {
272                let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
273                    return Err(syn::Error::new_spanned(
274                        &pat_type.pat,
275                        "#[shard] parameters must be simple named bindings like `foo: T`",
276                    ));
277                };
278                let is_router = pat_type
279                    .attrs
280                    .iter()
281                    .any(|attr| attr.path().is_ident("router"));
282                params.push(ShardParam {
283                    ident: pat_ident.ident.clone(),
284                    ty: (*pat_type.ty).clone(),
285                    is_router,
286                });
287            }
288        }
289    }
290    Ok(params)
291}
292
293#[cfg(feature = "shard")]
294fn is_state_router_controller_type(ty: &Type) -> bool {
295    let Type::Path(type_path) = ty else {
296        return false;
297    };
298    let Some(state_segment) = type_path.path.segments.last() else {
299        return false;
300    };
301    if state_segment.ident != "State" {
302        return false;
303    }
304    let syn::PathArguments::AngleBracketed(args) = &state_segment.arguments else {
305        return false;
306    };
307    let Some(syn::GenericArgument::Type(Type::Path(controller_path))) = args.args.first() else {
308        return false;
309    };
310    controller_path
311        .path
312        .segments
313        .last()
314        .is_some_and(|segment| segment.ident == "RouterController")
315}
316
317#[cfg(feature = "shard")]
318impl Parse for ShardMacroArgs {
319    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
320        let mut args = ShardMacroArgs::default();
321        while !input.is_empty() {
322            let key: Ident = input.parse()?;
323            input.parse::<Token![=]>()?;
324            match key.to_string().as_str() {
325                "crate_path" => {
326                    if args.crate_path.is_some() {
327                        return Err(syn::Error::new(
328                            key.span(),
329                            "duplicate `crate_path` argument",
330                        ));
331                    }
332                    args.crate_path = Some(input.parse::<Path>()?);
333                }
334                "shard_crate_path" => {
335                    if args.shard_crate_path.is_some() {
336                        return Err(syn::Error::new(
337                            key.span(),
338                            "duplicate `shard_crate_path` argument",
339                        ));
340                    }
341                    args.shard_crate_path = Some(input.parse::<Path>()?);
342                }
343                "state" => {
344                    if args.state_type.is_some() {
345                        return Err(syn::Error::new(key.span(), "duplicate `state` argument"));
346                    }
347                    args.state_type = Some(input.parse::<Type>()?);
348                }
349                "lifecycle" => {
350                    if args.lifecycle.is_some() {
351                        return Err(syn::Error::new(
352                            key.span(),
353                            "duplicate `lifecycle` argument",
354                        ));
355                    }
356                    args.lifecycle = Some(input.parse::<Ident>()?);
357                }
358                _ => {
359                    return Err(syn::Error::new(
360                        key.span(),
361                        "unsupported #[shard(...)] argument; expected `state`, `lifecycle`, `crate_path`, or `shard_crate_path`",
362                    ));
363                }
364            }
365
366            if input.peek(Token![,]) {
367                input.parse::<Token![,]>()?;
368            }
369        }
370        Ok(args)
371    }
372}
373
374/// Parse and validate strict component props signature:
375/// `fn component()` or `fn component(foo: T, bar: U)`.
376enum ComponentPropSignature {
377    Unit,
378    Params(Vec<PropFieldSpec>),
379}
380
381fn strict_prop_signature(sig: &syn::Signature) -> Result<ComponentPropSignature, syn::Error> {
382    if sig.inputs.is_empty() {
383        return Ok(ComponentPropSignature::Unit);
384    }
385
386    let mut fields = Vec::new();
387    for arg in &sig.inputs {
388        let FnArg::Typed(arg) = arg else {
389            return Err(syn::Error::new_spanned(
390                arg,
391                "#[tessera] methods are not supported; use free functions with named parameters",
392            ));
393        };
394
395        let Pat::Ident(pat_ident) = arg.pat.as_ref() else {
396            return Err(syn::Error::new_spanned(
397                &arg.pat,
398                "component parameters must be named identifiers",
399            ));
400        };
401
402        if pat_ident.by_ref.is_some() || pat_ident.mutability.is_some() {
403            return Err(syn::Error::new_spanned(
404                pat_ident,
405                "component parameters must be plain named bindings like `foo: T`",
406            ));
407        }
408
409        if matches!(arg.ty.as_ref(), Type::Reference(_)) {
410            return Err(syn::Error::new_spanned(
411                &arg.ty,
412                "component parameters must be owned types; borrowed parameter types are not supported",
413            ));
414        }
415
416        fields.push(PropFieldSpec {
417            ident: pat_ident.ident.clone(),
418            ty: (*arg.ty).clone(),
419            attrs: parse_prop_attr(&arg.attrs)?,
420            helper: infer_prop_helper_kind(arg.ty.as_ref()),
421        });
422    }
423
424    Ok(ComponentPropSignature::Params(fields))
425}
426
427fn prop_assert_tokens(crate_path: &syn::Path, prop_type: &syn::Type) -> proc_macro2::TokenStream {
428    quote! {
429        {
430            fn __tessera_assert_prop<T: #crate_path::__private::Prop>() {}
431            __tessera_assert_prop::<#prop_type>();
432        }
433    }
434}
435
436struct PropFieldSpec {
437    ident: Ident,
438    ty: Type,
439    attrs: PropAttrConfig,
440    helper: Option<PropHelperKind>,
441}
442
443fn is_required_component_field(field: &PropFieldSpec) -> bool {
444    option_inner_type(&field.ty).is_none()
445}
446
447fn stored_value_ty(field_ty: &Type) -> Type {
448    option_inner_type(field_ty).unwrap_or_else(|| field_ty.clone())
449}
450
451fn stored_field_ty(field_ty: &Type) -> Type {
452    let value_ty = stored_value_ty(field_ty);
453    parse_quote!(::core::option::Option<#value_ty>)
454}
455
456fn generate_constructor_param_and_assignment(
457    field: &PropFieldSpec,
458    crate_path: &Path,
459) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
460    let ident = &field.ident;
461    let field_ty = &field.ty;
462    let helper_uses_into = field.helper.is_some();
463
464    if let Some(helper) = &field.helper {
465        match helper {
466            PropHelperKind::CallbackWith { arg, ret } if is_unit_type(arg) => {
467                let param = quote! {
468                    #ident: impl Fn() -> #ret + Send + Sync + 'static
469                };
470                let assignment = quote! {
471                    __tessera_call.configure_deferred(stringify!(#ident), move |__tessera_pending_props| {
472                        __tessera_pending_props.#ident = Some(#crate_path::CallbackWith::new(move |()| #ident()));
473                    });
474                };
475                return Ok((param, assignment));
476            }
477            PropHelperKind::RenderSlotWith { arg } if is_unit_type(arg) => {
478                let output = format_ident!("__Tessera{}Output", pascal_case_ident(ident, ""));
479                let param = quote! {
480                    #ident: impl Fn() -> #output + Send + Sync + 'static
481                };
482                let assignment = quote! {
483                    __tessera_call.configure_deferred(stringify!(#ident), move |__tessera_pending_props| {
484                        __tessera_pending_props.#ident = Some(#crate_path::RenderSlotWith::new(move |()| #ident()));
485                    });
486                };
487                return Ok((param, assignment));
488            }
489            PropHelperKind::Callback
490            | PropHelperKind::CallbackWith { .. }
491            | PropHelperKind::RenderSlot
492            | PropHelperKind::RenderSlotWith { .. } => {}
493        }
494    }
495
496    let constructor_param = if helper_uses_into {
497        quote!(#ident: impl Into<#field_ty> + Send + Sync + 'static)
498    } else if field.attrs.into {
499        quote!(#ident: impl Into<#field_ty>)
500    } else {
501        quote!(#ident: #field_ty)
502    };
503    let constructor_assignment = if helper_uses_into {
504        quote! {
505            __tessera_call.configure_deferred(stringify!(#ident), move |__tessera_pending_props| {
506                __tessera_pending_props.#ident = Some(#ident.into());
507            });
508        }
509    } else {
510        let value = if field.attrs.into {
511            quote!(#ident.into())
512        } else {
513            quote!(#ident)
514        };
515        quote! {
516            let #ident = #value;
517            __tessera_call.configure(stringify!(#ident), move |__tessera_pending_props| __tessera_pending_props.#ident = Some(#ident));
518        }
519    };
520
521    Ok((constructor_param, constructor_assignment))
522}
523
524fn pascal_case_ident(base: &Ident, suffix: &str) -> Ident {
525    let mut output = String::new();
526    for segment in base
527        .to_string()
528        .split('_')
529        .filter(|segment| !segment.is_empty())
530    {
531        let mut chars = segment.chars();
532        if let Some(first) = chars.next() {
533            output.extend(first.to_uppercase());
534            output.push_str(chars.as_str());
535        }
536    }
537    output.push_str(suffix);
538    format_ident!("{output}")
539}
540
541fn hidden_props_ident(fn_name: &Ident) -> Ident {
542    format_ident!("__Tessera{}Props", pascal_case_ident(fn_name, ""))
543}
544
545fn builder_ident(fn_name: &Ident) -> Ident {
546    pascal_case_ident(fn_name, "Builder")
547}
548
549fn hidden_component_impl_ident(fn_name: &Ident) -> Ident {
550    format_ident!("__tessera_{}_impl", fn_name)
551}
552
553fn hidden_const_marker_ident(fn_name: &Ident, const_ident: &Ident) -> Ident {
554    format_ident!(
555        "__Tessera{}ConstMarker{}",
556        pascal_case_ident(fn_name, ""),
557        pascal_case_ident(const_ident, "")
558    )
559}
560
561fn validate_component_generics(generics: &Generics) -> syn::Result<()> {
562    for param in &generics.params {
563        if let GenericParam::Lifetime(param) = param {
564            return Err(syn::Error::new_spanned(
565                param,
566                "#[tessera] components do not support lifetime generics; use owned props instead",
567            ));
568        }
569    }
570    Ok(())
571}
572
573fn generic_marker_tokens(
574    fn_name: &Ident,
575    generics: &Generics,
576) -> (
577    Vec<proc_macro2::TokenStream>,
578    Option<proc_macro2::TokenStream>,
579) {
580    let mut const_marker_defs = Vec::new();
581    let mut marker_parts = Vec::new();
582
583    for param in &generics.params {
584        match param {
585            GenericParam::Type(param) => {
586                let ident = &param.ident;
587                marker_parts.push(quote!(#ident));
588            }
589            GenericParam::Const(param) => {
590                let ident = &param.ident;
591                let ty = &param.ty;
592                let marker_ident = hidden_const_marker_ident(fn_name, ident);
593                const_marker_defs.push(quote! {
594                    struct #marker_ident<const #ident: #ty>;
595                });
596                marker_parts.push(quote!(#marker_ident<#ident>));
597            }
598            GenericParam::Lifetime(_) => unreachable!("lifetime generics are rejected earlier"),
599        }
600    }
601
602    if marker_parts.is_empty() {
603        return (const_marker_defs, None);
604    }
605
606    let marker_ty = if marker_parts.len() == 1 {
607        let marker_part = &marker_parts[0];
608        quote!(#marker_part)
609    } else {
610        quote!((#(#marker_parts,)*))
611    };
612
613    (
614        const_marker_defs,
615        Some(quote! {
616            __tessera_generic_marker: ::core::marker::PhantomData<#marker_ty>
617        }),
618    )
619}
620
621fn generate_props_clone_default_impls(
622    type_name: &Ident,
623    generics: &Generics,
624    field_idents: &[Ident],
625    has_generic_marker: bool,
626) -> proc_macro2::TokenStream {
627    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
628    let clone_fields: Vec<_> = field_idents
629        .iter()
630        .map(|ident| quote!(#ident: self.#ident.clone()))
631        .collect();
632    let default_fields: Vec<_> = field_idents
633        .iter()
634        .map(|ident| quote!(#ident: ::core::default::Default::default()))
635        .collect();
636    let clone_marker = has_generic_marker.then(|| {
637        quote! {
638            __tessera_generic_marker: ::core::marker::PhantomData,
639        }
640    });
641    let default_marker = has_generic_marker.then(|| {
642        quote! {
643            __tessera_generic_marker: ::core::marker::PhantomData,
644        }
645    });
646
647    quote! {
648        impl #impl_generics ::core::clone::Clone for #type_name #ty_generics #where_clause {
649            fn clone(&self) -> Self {
650                Self {
651                    #(#clone_fields,)*
652                    #clone_marker
653                }
654            }
655        }
656
657        impl #impl_generics ::core::default::Default for #type_name #ty_generics #where_clause {
658            fn default() -> Self {
659                Self {
660                    #(#default_fields,)*
661                    #default_marker
662                }
663            }
664        }
665    }
666}
667
668fn generate_prop_like_impls(
669    type_name: &Ident,
670    component_impl: &Ident,
671    generics: &Generics,
672    fields: &[PropFieldSpec],
673    crate_path: &Path,
674) -> proc_macro2::TokenStream {
675    let compare_fields: Vec<_> = fields
676        .iter()
677        .filter(|field| !field.attrs.skip_eq)
678        .map(|field| field_compare_expr(&field.ident, &stored_field_ty(&field.ty)))
679        .collect();
680    let prop_eq_expr = if compare_fields.is_empty() {
681        quote! { true }
682    } else {
683        quote! { true #(&& #compare_fields)* }
684    };
685    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
686
687    quote! {
688        impl #impl_generics ::core::cmp::PartialEq for #type_name #ty_generics #where_clause {
689            fn eq(&self, other: &Self) -> bool {
690                #prop_eq_expr
691            }
692        }
693
694        impl #impl_generics #crate_path::__private::Prop for #type_name #ty_generics #where_clause {
695            fn prop_eq(&self, other: &Self) -> bool {
696                <Self as ::core::cmp::PartialEq>::eq(self, other)
697            }
698
699            fn run_component(self: ::std::sync::Arc<Self>) {
700                #component_impl(&self);
701            }
702        }
703    }
704}
705
706fn generate_builder_methods(
707    fields: &[PropFieldSpec],
708    crate_path: &syn::Path,
709) -> syn::Result<Vec<proc_macro2::TokenStream>> {
710    let mut methods = Vec::new();
711    for field in fields {
712        if field.attrs.skip_setter || is_required_component_field(field) {
713            continue;
714        }
715        let ident = &field.ident;
716        let ty = stored_value_ty(&field.ty);
717        let method_doc = format!("Configure `{ident}` before this component executes.");
718        if let Some(helper) = &field.helper {
719            let shared = format_ident!("{}_shared", ident);
720            let (generics, bound, value) = match helper {
721                PropHelperKind::Callback => (
722                    quote!(<F>),
723                    quote!(F: Fn() + Send + Sync + 'static),
724                    quote!(#crate_path::Callback::new(value)),
725                ),
726                PropHelperKind::CallbackWith { arg, ret } if is_unit_type(arg) => (
727                    quote!(<F>),
728                    quote!(F: Fn() -> #ret + Send + Sync + 'static),
729                    quote!(#crate_path::CallbackWith::new(move |()| value())),
730                ),
731                PropHelperKind::CallbackWith { arg, ret } => (
732                    quote!(<F>),
733                    quote!(F: Fn(#arg) -> #ret + Send + Sync + 'static),
734                    quote!(#crate_path::CallbackWith::new(value)),
735                ),
736                PropHelperKind::RenderSlot => (
737                    quote!(<F, Output>),
738                    quote!(F: Fn() -> Output + Send + Sync + 'static),
739                    quote!(#crate_path::RenderSlot::new(value)),
740                ),
741                PropHelperKind::RenderSlotWith { arg } if is_unit_type(arg) => (
742                    quote!(<F, Output>),
743                    quote!(F: Fn() -> Output + Send + Sync + 'static),
744                    quote!(#crate_path::RenderSlotWith::new(move |()| value())),
745                ),
746                PropHelperKind::RenderSlotWith { arg } => (
747                    quote!(<F, Output>),
748                    quote!(F: Fn(#arg) -> Output + Send + Sync + 'static),
749                    quote!(#crate_path::RenderSlotWith::new(value)),
750                ),
751            };
752            methods.push(quote! {
753                #[doc = #method_doc]
754                #[track_caller]
755                pub fn #ident #generics (&self, value: F) -> Self where #bound {
756                    self.props.configure_deferred(stringify!(#ident), move |props| {
757                        props.#ident = Some(#value);
758                    });
759                    Self { props: self.props }
760                }
761                #[doc = #method_doc]
762                #[track_caller]
763                pub fn #shared(&self, value: impl Into<#ty> + Send + Sync + 'static) -> Self {
764                    self.props.configure_deferred(stringify!(#ident), move |props| props.#ident = Some(value.into()));
765                    Self { props: self.props }
766                }
767            });
768        } else {
769            let parameter = if field.attrs.into {
770                quote!(impl Into<#ty>)
771            } else {
772                quote!(#ty)
773            };
774            let value = if field.attrs.into {
775                quote!(value.into())
776            } else {
777                quote!(value)
778            };
779            methods.push(quote! {
780                #[doc = #method_doc]
781                #[track_caller]
782                pub fn #ident(&self, value: #parameter) -> Self {
783                    let value = #value;
784                    self.props.configure(stringify!(#ident), move |props| props.#ident = Some(value));
785                    Self { props: self.props }
786                }
787            });
788        }
789        let optional = format_ident!("{}_optional", ident);
790        methods.push(quote! {
791            #[doc = #method_doc]
792            #[track_caller]
793            pub fn #optional(&self, value: Option<#ty>) -> Self {
794                self.props.configure(stringify!(#ident), move |props| props.#ident = value);
795                Self { props: self.props }
796            }
797        });
798    }
799    Ok(methods)
800}
801
802fn component_type_id_tokens(
803    fn_name: &syn::Ident,
804    prop_type: &syn::Type,
805) -> proc_macro2::TokenStream {
806    quote! {
807        {
808            use std::{any::TypeId, hash::{Hash, Hasher}};
809            let mut hasher = std::collections::hash_map::DefaultHasher::new();
810            module_path!().hash(&mut hasher);
811            stringify!(#fn_name).hash(&mut hasher);
812            TypeId::of::<#prop_type>().hash(&mut hasher);
813            hasher.finish()
814        }
815    }
816}
817
818struct ControlFlowInstrumenter {
819    counter: u64,
820}
821
822impl ControlFlowInstrumenter {
823    fn new() -> Self {
824        Self { counter: 1 }
825    }
826
827    fn next_group_id(&mut self) -> u64 {
828        let id = self.counter;
829        self.counter += 1;
830        id
831    }
832
833    fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
834        self.visit_expr_mut(expr);
835        let group_id = self.next_group_id();
836        // Use fully-qualified path ::tessera_ui to avoid relying on a crate
837        // alias
838        let original_expr = &expr;
839        let new_expr: Expr = parse_quote! {
840            {
841                let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
842                #original_expr
843            }
844        };
845        *expr = new_expr;
846    }
847
848    fn wrap_block_in_group(&mut self, block: &mut Block) {
849        self.visit_block_mut(block);
850
851        let group_id = self.next_group_id();
852        let original_stmts = &block.stmts;
853
854        let new_block: Block = parse_quote! {
855            {
856                let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
857                #(#original_stmts)*
858            }
859        };
860
861        *block = new_block;
862    }
863
864    fn wrap_block_in_path_group(&mut self, block: &mut Block) {
865        self.visit_block_mut(block);
866
867        let group_id = self.next_group_id();
868        let original_stmts = &block.stmts;
869
870        let new_block: Block = parse_quote! {
871            {
872                let _group_guard = ::tessera_ui::__private::PathGroupGuard::new(#group_id);
873                #(#original_stmts)*
874            }
875        };
876
877        *block = new_block;
878    }
879}
880
881impl VisitMut for ControlFlowInstrumenter {
882    fn visit_expr_if_mut(&mut self, i: &mut syn::ExprIf) {
883        self.visit_expr_mut(&mut i.cond);
884        self.wrap_block_in_group(&mut i.then_branch);
885        if let Some((_, else_branch)) = &mut i.else_branch {
886            match &mut **else_branch {
887                Expr::Block(block_expr) => {
888                    self.wrap_block_in_group(&mut block_expr.block);
889                }
890                Expr::If(_) => {
891                    self.visit_expr_mut(else_branch);
892                }
893                _ => {
894                    self.wrap_expr_in_group(else_branch);
895                }
896            }
897        }
898    }
899
900    fn visit_expr_match_mut(&mut self, m: &mut syn::ExprMatch) {
901        self.visit_expr_mut(&mut m.expr);
902        for arm in &mut m.arms {
903            self.wrap_expr_in_group(&mut arm.body);
904        }
905    }
906
907    fn visit_expr_for_loop_mut(&mut self, f: &mut syn::ExprForLoop) {
908        self.visit_expr_mut(&mut f.expr);
909        self.wrap_block_in_path_group(&mut f.body);
910    }
911
912    fn visit_expr_while_mut(&mut self, w: &mut syn::ExprWhile) {
913        self.visit_expr_mut(&mut w.cond);
914        self.wrap_block_in_path_group(&mut w.body);
915    }
916
917    fn visit_expr_loop_mut(&mut self, l: &mut syn::ExprLoop) {
918        self.wrap_block_in_path_group(&mut l.body);
919    }
920}
921
922/// Transforms a regular Rust function into a Tessera UI component.
923///
924/// # Usage
925///
926/// Annotate a plain free function with `#[tessera]`.
927///
928/// The macro turns the function into a Tessera component entrypoint. Public
929/// components use the generated builder syntax, while the original function
930/// body runs inside Tessera's build/replay context.
931///
932/// # Parameters
933///
934/// * Attribute arguments select the Tessera crate path. Use `#[tessera]` for
935///   normal external authoring, or `#[tessera(crate)]` inside Tessera crates.
936///
937/// # When NOT to Use
938///
939/// * For functions that should not participate in the component tree.
940///
941/// # See Also
942///
943/// * [`#[shard]`](crate::shard) for navigation‑aware components with injectable
944///   shard state.
945#[proc_macro_attribute]
946pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
947    let ComponentAttr { crate_path, layout } = parse_macro_input!(attr as ComponentAttr);
948    let begin_scope = if layout {
949        quote!(#crate_path::__private::begin_layout_component_node)
950    } else {
951        quote!(#crate_path::__private::begin_component_node)
952    };
953
954    let mut input_fn = parse_macro_input!(item as ItemFn);
955    if let Some(conflicting_attr) = input_fn
956        .attrs
957        .iter()
958        .find(|attr| attr.path().is_ident("shard"))
959    {
960        return syn::Error::new_spanned(
961            conflicting_attr,
962            "#[tessera] and #[shard] cannot be combined on the same function; use #[shard] only",
963        )
964        .to_compile_error()
965        .into();
966    }
967
968    if input_fn.sig.constness.is_some()
969        || input_fn.sig.asyncness.is_some()
970        || !matches!(input_fn.sig.safety, syn::Safety::Default)
971        || input_fn.sig.abi.is_some()
972        || input_fn.sig.variadic.is_some()
973    {
974        return syn::Error::new_spanned(
975            &input_fn.sig,
976            "#[tessera] components must be plain free functions",
977        )
978        .to_compile_error()
979        .into();
980    }
981    if !matches!(input_fn.sig.output, syn::ReturnType::Default) {
982        return syn::Error::new_spanned(
983            &input_fn.sig.output,
984            "#[tessera] components must not return a value",
985        )
986        .to_compile_error()
987        .into();
988    }
989
990    let fn_name = &input_fn.sig.ident;
991    let fn_vis = &input_fn.vis;
992    let fn_attrs = &input_fn.attrs;
993    if let Err(err) = validate_component_generics(&input_fn.sig.generics) {
994        return err.to_compile_error().into();
995    }
996    let prop_signature = match strict_prop_signature(&input_fn.sig) {
997        Ok(v) => v,
998        Err(err) => return err.to_compile_error().into(),
999    };
1000
1001    let mut instrumenter = ControlFlowInstrumenter::new();
1002    instrumenter.visit_block_mut(&mut input_fn.block);
1003    let fn_block = &input_fn.block;
1004    let generics = &input_fn.sig.generics;
1005    let mut item_generics = generics.clone();
1006    item_generics.where_clause = None;
1007    let where_clause = &generics.where_clause;
1008    let (impl_generics, ty_generics, _) = generics.split_for_impl();
1009
1010    let fields = match &prop_signature {
1011        ComponentPropSignature::Unit => &[][..],
1012        ComponentPropSignature::Params(fields) => fields.as_slice(),
1013    };
1014    let expanded = {
1015        let props_ident = hidden_props_ident(fn_name);
1016        let builder_ident = builder_ident(fn_name);
1017        let impl_ident = hidden_component_impl_ident(fn_name);
1018        let (const_marker_defs, marker_field_def) = generic_marker_tokens(fn_name, generics);
1019        let has_generic_marker = marker_field_def.is_some();
1020        let prop_type: syn::Type = syn::parse_quote!(#props_ident #ty_generics);
1021        let uncached_type_id = component_type_id_tokens(fn_name, &prop_type);
1022        let (metadata, component_type_id_tokens) = if generics.params.is_empty() {
1023            (
1024                quote! {
1025                    impl #props_ident {
1026                        fn __tessera_type_id() -> u64 {
1027                            static TYPE_ID: ::std::sync::OnceLock<u64> = ::std::sync::OnceLock::new();
1028                            *TYPE_ID.get_or_init(|| #uncached_type_id)
1029                        }
1030                    }
1031                },
1032                quote! { #props_ident::__tessera_type_id() },
1033            )
1034        } else {
1035            (quote! {}, uncached_type_id)
1036        };
1037        let field_idents: Vec<_> = fields.iter().map(|field| field.ident.clone()).collect();
1038        let required_fields: Vec<_> = fields
1039            .iter()
1040            .filter(|field| is_required_component_field(field))
1041            .collect();
1042        let mut entry_generics = item_generics.clone();
1043        for field in &required_fields {
1044            if matches!(&field.helper, Some(PropHelperKind::RenderSlotWith { arg }) if is_unit_type(arg))
1045            {
1046                let output =
1047                    format_ident!("__Tessera{}Output", pascal_case_ident(&field.ident, ""));
1048                entry_generics.params.push(syn::parse_quote!(#output));
1049            }
1050        }
1051        // Conversions can run user code after registration. Preserve their
1052        // order relative to later property assignments and their destructors.
1053        let initial_count = required_fields
1054            .iter()
1055            .take_while(|field| field.helper.is_none() && !field.attrs.into)
1056            .count();
1057        let (initial_fields, pending_fields) = required_fields.split_at(initial_count);
1058        let constructor_params_and_assignments: Vec<_> = match pending_fields
1059            .iter()
1060            .map(|field| generate_constructor_param_and_assignment(field, &crate_path))
1061            .collect::<syn::Result<Vec<_>>>()
1062        {
1063            Ok(entries) => entries,
1064            Err(err) => return err.to_compile_error().into(),
1065        };
1066        let constructor_params: Vec<_> = initial_fields
1067            .iter()
1068            .map(|field| {
1069                let ident = &field.ident;
1070                let ty = &field.ty;
1071                quote!(#ident: #ty)
1072            })
1073            .chain(
1074                constructor_params_and_assignments
1075                    .iter()
1076                    .map(|(param, _)| param.clone()),
1077            )
1078            .collect();
1079        let constructor_assignments: Vec<_> = constructor_params_and_assignments
1080            .iter()
1081            .map(|(_, assignment)| assignment.clone())
1082            .collect();
1083        let initial_props = if initial_fields.is_empty() {
1084            quote!(<#props_ident #ty_generics>::default())
1085        } else {
1086            let initializers = initial_fields.iter().map(|field| {
1087                let ident = &field.ident;
1088                quote!(#ident: Some(#ident),)
1089            });
1090            quote!(#props_ident {
1091                #(#initializers)*
1092                ..<#props_ident #ty_generics>::default()
1093            })
1094        };
1095        let field_defs: Vec<_> = fields
1096            .iter()
1097            .map(|field| {
1098                let ident = &field.ident;
1099                let ty = stored_field_ty(&field.ty);
1100                quote!(#ident: #ty)
1101            })
1102            .collect();
1103        let field_resolutions: Vec<_> = fields
1104            .iter()
1105            .map(|field| {
1106                let ident = &field.ident;
1107                let ty = &field.ty;
1108                if option_inner_type(ty).is_some() {
1109                    return quote! {
1110                        let #ident: #ty = __tessera_props.#ident.clone();
1111                    };
1112                }
1113
1114                let missing_prop_message = format!(
1115                    "missing required prop `{}` for component `{}`",
1116                    ident, fn_name
1117                );
1118                quote! {
1119                    let #ident: #ty = __tessera_props.#ident.clone().unwrap_or_else(|| {
1120                        panic!(#missing_prop_message)
1121                    });
1122                }
1123            })
1124            .collect();
1125        let prop_impl_tokens =
1126            generate_prop_like_impls(&props_ident, &impl_ident, generics, fields, &crate_path);
1127        let builder_methods = match generate_builder_methods(fields, &crate_path) {
1128            Ok(methods) => methods,
1129            Err(err) => return err.to_compile_error().into(),
1130        };
1131        let prop_assert_tokens = prop_assert_tokens(&crate_path, &prop_type);
1132        let marker_field_def = marker_field_def
1133            .map(|field| quote!(#field,))
1134            .unwrap_or_default();
1135        let props_clone_default_impls = generate_props_clone_default_impls(
1136            &props_ident,
1137            generics,
1138            &field_idents,
1139            has_generic_marker,
1140        );
1141
1142        quote! {
1143            #(#const_marker_defs)*
1144
1145            struct #props_ident #item_generics #where_clause {
1146                #(#field_defs,)*
1147                #marker_field_def
1148            }
1149
1150            #props_clone_default_impls
1151            #prop_impl_tokens
1152            #metadata
1153
1154            #[doc = concat!("Configuration handle returned by [`", stringify!(#fn_name), "`].")]
1155            #fn_vis struct #builder_ident #item_generics #where_clause {
1156                props: #crate_path::__private::ComponentCall<#props_ident #ty_generics>,
1157            }
1158
1159            impl #impl_generics #builder_ident #ty_generics #where_clause {
1160                #(#builder_methods)*
1161            }
1162
1163            #(#fn_attrs)*
1164            #fn_vis fn #fn_name #entry_generics(
1165                #(#constructor_params),*
1166            ) -> #builder_ident #ty_generics #where_clause {
1167                let __tessera_call = #crate_path::__private::register_component_call(
1168                        stringify!(#fn_name), #component_type_id_tokens,
1169                        #initial_props,
1170                    );
1171                #(#constructor_assignments)*
1172                #builder_ident { props: __tessera_call }
1173            }
1174
1175            fn #impl_ident #item_generics(__tessera_props: &::std::sync::Arc<#props_ident #ty_generics>) #where_clause {
1176                #prop_assert_tokens
1177                let __tessera_scope = #begin_scope(
1178                    stringify!(#fn_name),
1179                    #component_type_id_tokens,
1180                    __tessera_props,
1181                );
1182                if __tessera_scope.is_retained() {
1183                    return;
1184                }
1185                #crate_path::__private::with_pending_scope(|| {
1186                    #crate_path::__private::record_component_execution(
1187                        stringify!(#fn_name),
1188                        __tessera_scope.instance_logic_id(),
1189                    );
1190                    #(#field_resolutions)*
1191                    #fn_block;
1192                });
1193            }
1194        }
1195    };
1196
1197    TokenStream::from(expanded)
1198}
1199
1200/// Sets Tessera lint levels for an item or inline module tree.
1201///
1202/// Use `#[tessera_ui::lint(allow(tessera::redundant_render_slot))]` with
1203/// `allow`, `warn`, `deny`, or `forbid`. Lints begin at their default levels;
1204/// nested item settings override inherited levels unless forbidden.
1205/// `cargo tessera check` applies these settings with rustc lint-level precedence,
1206/// including command-line `--force-warn` and `--cap-lints` controls.
1207#[proc_macro_attribute]
1208pub fn lint(_attr: TokenStream, item: TokenStream) -> TokenStream {
1209    item
1210}
1211
1212/// Generates platform-specific entry points from a shared `run` function.
1213///
1214/// # Usage
1215///
1216/// Annotate a public zero-argument function that returns
1217/// `tessera_ui::EntryPoint`.
1218#[proc_macro_attribute]
1219pub fn entry(attr: TokenStream, item: TokenStream) -> TokenStream {
1220    let crate_path: syn::Path = match parse_crate_path(attr) {
1221        Ok(path) => path,
1222        Err(err) => return err.to_compile_error().into(),
1223    };
1224    let mut input_fn = parse_macro_input!(item as ItemFn);
1225
1226    if !input_fn.sig.inputs.is_empty() {
1227        return syn::Error::new_spanned(
1228            &input_fn.sig.inputs,
1229            "entry functions must not accept arguments",
1230        )
1231        .to_compile_error()
1232        .into();
1233    }
1234
1235    input_fn.attrs.retain(|attr| !attr.path().is_ident("entry"));
1236    let fn_name = &input_fn.sig.ident;
1237
1238    let expanded = quote! {
1239        #input_fn
1240
1241        #[cfg(target_os = "android")]
1242        #[unsafe(no_mangle)]
1243        fn android_main(android_app: #crate_path::winit::platform::android::activity::AndroidApp) {
1244            if let Err(err) = #fn_name().run_android(android_app) {
1245                eprintln!("App failed to run: {err}");
1246            }
1247        }
1248    };
1249
1250    expanded.into()
1251}
1252
1253/// Transforms a function into a *shard component* that can be navigated to via
1254/// the routing system and (optionally) provided with a lazily‑initialized
1255/// per‑shard state.
1256///
1257/// # Features
1258///
1259/// * Generates a `StructNameDestination` (UpperCamelCase + `Destination`)
1260///   implementing `tessera_shard::router::RouterDestination`
1261/// * Optional state injection via `#[shard(state = T)]`, where `T`:
1262///   - Must implement `Default + Send + Sync + 'static`
1263///   - Is constructed (or reused) and exposed as local variable `state` with
1264///     type `tessera_shard::ShardState<T>`
1265/// * Produces a stable shard ID: `module_path!()::function_name`
1266///
1267/// # Lifecycle
1268///
1269/// Controlled by the `lifecycle` shard attribute argument.
1270/// * Default: `Shard` – state is removed when the destination is `pop()`‑ed
1271/// * Override: `#[shard(lifecycle = scope)]` to persist for the lifetime of the
1272///   current router controller hosted by `shard_home`
1273///
1274/// Route-scoped state is removed on route pop/clear. Scope-scoped state is
1275/// removed when the hosting `shard_home` is dropped.
1276///
1277/// # Parameter Transformation
1278///
1279/// * Function parameters are treated as explicit destination props.
1280/// * When `state = T` is configured, shard state is injected as local variable
1281///   `state` and does not appear in the function signature.
1282///
1283/// # Errors
1284///
1285/// Emits a compile error if unsupported `lifecycle` is provided, or if
1286/// `lifecycle` is used without `state`.
1287#[cfg(feature = "shard")]
1288#[proc_macro_attribute]
1289pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
1290    use heck::ToUpperCamelCase;
1291    let shard_args = if attr.is_empty() {
1292        ShardMacroArgs::default()
1293    } else {
1294        match syn::parse::<ShardMacroArgs>(attr) {
1295            Ok(value) => value,
1296            Err(err) => return err.to_compile_error().into(),
1297        }
1298    };
1299
1300    let ui_crate_path: syn::Path = shard_args
1301        .crate_path
1302        .unwrap_or_else(|| syn::parse_quote!(::tessera_ui));
1303    let shard_crate_path: syn::Path = shard_args
1304        .shard_crate_path
1305        .unwrap_or_else(|| syn::parse_quote!(::tessera_shard));
1306
1307    let func = parse_macro_input!(input as ItemFn);
1308    if let Some(conflicting_attr) = func
1309        .attrs
1310        .iter()
1311        .find(|attr| attr.path().is_ident("tessera"))
1312    {
1313        return syn::Error::new_spanned(
1314            conflicting_attr,
1315            "#[shard] already defines a component boundary; do not combine #[shard] with #[tessera]",
1316        )
1317        .to_compile_error()
1318        .into();
1319    }
1320
1321    let shard_params = match parse_shard_params(&func.sig) {
1322        Ok(params) => params,
1323        Err(err) => return err.to_compile_error().into(),
1324    };
1325    let router_params: Vec<_> = shard_params
1326        .iter()
1327        .filter(|param| param.is_router)
1328        .collect();
1329    if router_params.len() > 1 {
1330        return syn::Error::new_spanned(
1331            &func.sig.inputs,
1332            "#[shard] supports at most one `#[router]` parameter",
1333        )
1334        .to_compile_error()
1335        .into();
1336    }
1337    if let Some(router_param) = router_params.first()
1338        && !is_state_router_controller_type(&router_param.ty)
1339    {
1340        return syn::Error::new_spanned(
1341            &router_param.ty,
1342            "`#[router]` parameters must have type `State<RouterController>`",
1343        )
1344        .to_compile_error()
1345        .into();
1346    }
1347
1348    let state_type = shard_args.state_type;
1349    let state_lifecycle_tokens = match shard_args.lifecycle {
1350        Some(lifecycle) => {
1351            let lifecycle_name = lifecycle.to_string().to_lowercase();
1352            if state_type.is_none() {
1353                return syn::Error::new_spanned(
1354                    lifecycle,
1355                    "`lifecycle` requires `state` in #[shard(...)]",
1356                )
1357                .to_compile_error()
1358                .into();
1359            }
1360            if lifecycle_name == "scope" {
1361                quote! { #shard_crate_path::ShardStateLifeCycle::Scope }
1362            } else if lifecycle_name == "shard" {
1363                quote! { #shard_crate_path::ShardStateLifeCycle::Shard }
1364            } else {
1365                return syn::Error::new_spanned(
1366                    lifecycle,
1367                    "unsupported `lifecycle` in #[shard(...)]: expected `scope` or `shard`",
1368                )
1369                .to_compile_error()
1370                .into();
1371            }
1372        }
1373        None => quote! { #shard_crate_path::ShardStateLifeCycle::Shard },
1374    };
1375
1376    let func_body = func.block;
1377    let func_name_str = func.sig.ident.to_string();
1378
1379    let func_attrs = &func.attrs;
1380    let func_vis = &func.vis;
1381    let router_controller_resolution = quote! {
1382        __tessera_router_controller
1383    };
1384
1385    let router_binding =
1386        router_params
1387            .first()
1388            .map_or_else(proc_macro2::TokenStream::new, |param| {
1389                let router_ident = &param.ident;
1390                quote! {
1391                    let #router_ident = #router_ident
1392                        .unwrap_or_else(|| #router_controller_resolution);
1393                }
1394            });
1395    let mut func_sig_modified = func.sig.clone();
1396    func_sig_modified.inputs.insert(0, parse_quote!(
1397        #[prop(skip_setter)] __tessera_router_controller: #ui_crate_path::State<#shard_crate_path::router::RouterController>
1398    ));
1399    for input in &mut func_sig_modified.inputs {
1400        let FnArg::Typed(pat_type) = input else {
1401            continue;
1402        };
1403        let is_router = pat_type
1404            .attrs
1405            .iter()
1406            .any(|attr| attr.path().is_ident("router"));
1407        if !is_router {
1408            continue;
1409        }
1410        let original_ty = (*pat_type.ty).clone();
1411        *pat_type.ty = parse_quote!(::core::option::Option<#original_ty>);
1412        pat_type
1413            .attrs
1414            .retain(|attr| !attr.path().is_ident("router"));
1415        pat_type.attrs.push(syn::parse_quote!(#[prop(skip_setter)]));
1416    }
1417
1418    // Generate struct name for the new RouterDestination
1419    let func_name = func.sig.ident.clone();
1420    let struct_name = syn::Ident::new(
1421        &format!("{}Destination", func_name_str.to_upper_camel_case()),
1422        func_name.span(),
1423    );
1424    // Generate fields for the new struct that will implement
1425    // `RouterDestination`
1426    let dest_fields = shard_params
1427        .iter()
1428        .filter(|param| !param.is_router)
1429        .map(|param| {
1430            let ident = &param.ident;
1431            let ty = &param.ty;
1432            quote! { pub #ident: #ty }
1433        });
1434
1435    let expanded = {
1436        let required_args = shard_params
1437            .iter()
1438            .filter(|param| !param.is_router && option_inner_type(&param.ty).is_none())
1439            .map(|param| {
1440                let ident = &param.ident;
1441                quote! { self.#ident.clone() }
1442            });
1443        let destination_builder_setters = shard_params
1444            .iter()
1445            .filter(|param| !param.is_router && option_inner_type(&param.ty).is_some())
1446            .map(|param| {
1447                let ident = &param.ident;
1448                quote! {
1449                    if let Some(value) = self.#ident.clone() {
1450                        __tessera_builder.#ident(value);
1451                    }
1452                }
1453            });
1454
1455        if let Some(state_type) = state_type {
1456            quote! {
1457                #[derive(Clone, PartialEq)]
1458                #func_vis struct #struct_name {
1459                    #(#dest_fields),*
1460                }
1461
1462                impl #shard_crate_path::router::RouterDestination for #struct_name {
1463                    fn exec_component(&self, controller: #ui_crate_path::State<#shard_crate_path::router::RouterController>) {
1464                        let __tessera_builder = #func_name(controller #(, #required_args)*);
1465                        #(#destination_builder_setters)*
1466                    }
1467
1468                    fn exec_component_handle(&self) -> std::sync::Arc<dyn Fn(#ui_crate_path::State<#shard_crate_path::router::RouterController>) + Send + Sync> {
1469                        let destination = self.clone();
1470                        std::sync::Arc::new(move |controller| destination.exec_component(controller))
1471                    }
1472
1473                    fn destination_id() -> &'static str {
1474                        concat!(module_path!(), "::", #func_name_str)
1475                    }
1476                }
1477
1478                #(#func_attrs)*
1479                #[#ui_crate_path::tessera(#ui_crate_path)]
1480                #func_vis #func_sig_modified {
1481                    const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
1482                    let __router_controller = #router_controller_resolution;
1483                    #router_binding
1484                    let __route_scope = #ui_crate_path::use_context::<#shard_crate_path::__private::RouteRenderScope>().map(|scope| scope.get());
1485                    #shard_crate_path::__private::with_rendered_router_shard_state::<#state_type, _, _>(
1486                        SHARD_ID,
1487                        #state_lifecycle_tokens,
1488                        __router_controller,
1489                        __route_scope,
1490                        |state| {
1491                            #func_body
1492                        },
1493                    )
1494                }
1495            }
1496        } else {
1497            quote! {
1498                #[derive(Clone, PartialEq)]
1499                #func_vis struct #struct_name {
1500                    #(#dest_fields),*
1501                }
1502
1503                impl #shard_crate_path::router::RouterDestination for #struct_name {
1504                    fn exec_component(&self, controller: #ui_crate_path::State<#shard_crate_path::router::RouterController>) {
1505                        let __tessera_builder = #func_name(controller #(, #required_args)*);
1506                        #(#destination_builder_setters)*
1507                    }
1508
1509                    fn exec_component_handle(&self) -> std::sync::Arc<dyn Fn(#ui_crate_path::State<#shard_crate_path::router::RouterController>) + Send + Sync> {
1510                        let destination = self.clone();
1511                        std::sync::Arc::new(move |controller| destination.exec_component(controller))
1512                    }
1513
1514                    fn destination_id() -> &'static str {
1515                        concat!(module_path!(), "::", #func_name_str)
1516                    }
1517                }
1518
1519                #(#func_attrs)*
1520                #[#ui_crate_path::tessera(#ui_crate_path)]
1521                #func_vis #func_sig_modified {
1522                    #router_binding
1523                    #func_body
1524                }
1525            }
1526        }
1527    };
1528
1529    TokenStream::from(expanded)
1530}