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