1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use callback_param::CallbackParam;
5use held_callback::{HeldCallback, borrows};
6use packed_params::PackedParams;
7use proc_macro::TokenStream;
8use proc_macro_crate::{FoundCrate, crate_name};
9use proc_macro2::{Span, TokenStream as TokenStream2};
10use quote::quote;
11use returning_call::ReturningCall;
12use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Type, parse_macro_input};
13
14mod branch_groups;
15mod callback_param;
16mod constant_modifiers;
17mod held_callback;
18#[cfg(feature = "localization")]
19mod localization;
20mod packed_params;
21mod preview;
22mod returning_call;
23mod worker_job;
24
25#[cfg(feature = "localization")]
29#[proc_macro]
30pub fn tr(input: TokenStream) -> TokenStream {
31 localization::translate(input.into())
32 .unwrap_or_else(|error| error.to_compile_error())
33 .into()
34}
35
36#[cfg(feature = "localization")]
39#[proc_macro]
40pub fn translations(input: TokenStream) -> TokenStream {
41 localization::catalogs(input.into())
42 .unwrap_or_else(|error| error.to_compile_error())
43 .into()
44}
45
46#[cfg(feature = "localization")]
49#[proc_macro]
50pub fn message(input: TokenStream) -> TokenStream {
51 localization::message(input.into())
52 .unwrap_or_else(|error| error.to_compile_error())
53 .into()
54}
55
56#[cfg(feature = "localization")]
59#[proc_macro]
60pub fn translation_messages(input: TokenStream) -> TokenStream {
61 localization::bindings(input.into())
62 .unwrap_or_else(|error| error.to_compile_error())
63 .into()
64}
65#[cfg(test)]
66#[path = "tests/preview_tests.rs"]
67mod preview_tests;
68
69#[proc_macro_attribute]
78pub fn worker_job(_args: TokenStream, item: TokenStream) -> TokenStream {
79 let item = syn::parse_macro_input!(item as ItemFn);
80 worker_job::expand(&item)
81 .unwrap_or_else(|error| error.to_compile_error())
82 .into()
83}
84
85#[proc_macro_attribute]
90pub fn preview(args: TokenStream, item: TokenStream) -> TokenStream {
91 let function = parse_macro_input!(item as ItemFn);
92 preview::expand(args.into(), function)
93 .unwrap_or_else(|error| error.to_compile_error())
94 .into()
95}
96
97fn is_fn_like_type(ty: &Type) -> bool {
98 match ty {
99 Type::ImplTrait(impl_trait) => impl_trait.bounds.iter().any(|bound| {
100 if let syn::TypeParamBound::Trait(trait_bound) = bound {
101 let path = &trait_bound.path;
102 if let Some(segment) = path.segments.last() {
103 let ident_str = segment.ident.to_string();
104 return ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce";
105 }
106 }
107 false
108 }),
109 Type::Path(type_path) => {
110 if let Some(segment) = type_path.path.segments.last()
111 && segment.ident == "Box"
112 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
113 && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
114 args.args.first()
115 {
116 return trait_obj.bounds.iter().any(|bound| {
117 if let syn::TypeParamBound::Trait(trait_bound) = bound {
118 let path = &trait_bound.path;
119 if let Some(segment) = path.segments.last() {
120 let ident_str = segment.ident.to_string();
121 return ident_str == "FnMut"
122 || ident_str == "Fn"
123 || ident_str == "FnOnce";
124 }
125 }
126 false
127 });
128 }
129 false
130 }
131 Type::FnPtr(_) => true,
132 _ => false,
133 }
134}
135
136fn is_generic_fn_like(ty: &Type, generics: &syn::Generics) -> bool {
137 let type_ident = match ty {
138 Type::Path(type_path) if type_path.path.segments.len() == 1 => {
139 &type_path.path.segments[0].ident
140 }
141 _ => return false,
142 };
143
144 for param in &generics.params {
145 if let syn::GenericParam::Type(type_param) = param
146 && type_param.ident == *type_ident
147 {
148 for bound in &type_param.bounds {
149 if let syn::TypeParamBound::Trait(trait_bound) = bound
150 && let Some(segment) = trait_bound.path.segments.last()
151 {
152 let ident_str = segment.ident.to_string();
153 if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
154 return true;
155 }
156 }
157 }
158 }
159 }
160
161 if let Some(where_clause) = &generics.where_clause {
162 for predicate in &where_clause.predicates {
163 if let syn::WherePredicate::Type(pred) = predicate
164 && let Type::Path(bounded_type) = &pred.bounded_ty
165 && bounded_type.path.segments.len() == 1
166 && bounded_type.path.segments[0].ident == *type_ident
167 {
168 for bound in &pred.bounds {
169 if let syn::TypeParamBound::Trait(trait_bound) = bound
170 && let Some(segment) = trait_bound.path.segments.last()
171 {
172 let ident_str = segment.ident.to_string();
173 if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
174 return true;
175 }
176 }
177 }
178 }
179 }
180 }
181
182 false
183}
184
185struct ParamInfo {
187 ident: Ident,
188 pat: Box<Pat>,
189 ty: Type,
190 pat_is_mut: bool,
191 is_impl_trait: bool,
192}
193
194fn is_fn_param(ty: &Type, generics: &syn::Generics) -> bool {
195 is_fn_like_type(ty) || is_generic_fn_like(ty, generics)
196}
197
198fn type_bare_generic_ident(ty: &Type) -> Option<&Ident> {
199 match ty {
200 Type::Path(type_path)
201 if type_path.qself.is_none()
202 && type_path.path.segments.len() == 1
203 && type_path.path.segments[0].arguments.is_none() =>
204 {
205 Some(&type_path.path.segments[0].ident)
206 }
207 _ => None,
208 }
209}
210
211fn stream_mentions_ident(tokens: &TokenStream2, name: &str) -> bool {
212 tokens.clone().into_iter().any(|tt| match tt {
213 proc_macro2::TokenTree::Ident(ident) => ident == name,
214 proc_macro2::TokenTree::Group(group) => stream_mentions_ident(&group.stream(), name),
215 _ => false,
216 })
217}
218
219fn filter_generics(
220 generics: &syn::Generics,
221 strip: &std::collections::BTreeSet<String>,
222) -> syn::Generics {
223 let mut filtered = generics.clone();
224 filtered.params = filtered
225 .params
226 .into_iter()
227 .filter(|param| match param {
228 syn::GenericParam::Type(type_param) => !strip.contains(&type_param.ident.to_string()),
229 _ => true,
230 })
231 .collect();
232 if let Some(where_clause) = &mut filtered.where_clause {
233 where_clause.predicates = where_clause
234 .predicates
235 .clone()
236 .into_iter()
237 .filter(|predicate| {
238 if let syn::WherePredicate::Type(pred) = predicate
239 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
240 {
241 return !strip.contains(&ident.to_string());
242 }
243 true
244 })
245 .collect();
246 if where_clause.predicates.is_empty() {
247 filtered.where_clause = None;
248 }
249 }
250 filtered
251}
252
253fn is_node_id_return(ty: &Type) -> bool {
254 matches!(
255 ty,
256 Type::Path(type_path)
257 if type_path
258 .path
259 .segments
260 .last()
261 .is_some_and(|segment| segment.ident == "NodeId")
262 )
263}
264
265fn core_crate_path() -> TokenStream2 {
266 let crate_name = crate_name("cranpose")
267 .ok()
268 .or_else(|| crate_name("cranpose-core").ok());
269
270 match crate_name {
271 Some(FoundCrate::Itself) => quote!(crate),
272 Some(FoundCrate::Name(name)) => {
273 let ident = Ident::new(&name, Span::call_site());
274 quote!(#ident)
275 }
276 None => quote!(cranpose_core),
277 }
278}
279
280fn slot_key(core_path: &TokenStream2) -> TokenStream2 {
284 if cfg!(feature = "hot-reload") {
285 return quote! { #core_path::caller_location_key() };
286 }
287 quote! {{
288 const __CRANPOSE_SLOT_KEY: #core_path::Key =
289 #core_path::const_location_key(file!(), line!(), column!());
290 #core_path::noted_location_key(__CRANPOSE_SLOT_KEY, file!(), line!(), column!())
291 }}
292}
293
294fn definition_key_stmt(
295 core_path: &TokenStream2,
296 caller_key_ident: &Ident,
297 name: &Ident,
298) -> TokenStream2 {
299 if cfg!(feature = "hot-reload") {
300 let name = name.to_string();
303 return quote! {
304 let #caller_key_ident = #core_path::composable_identity_key({
305 const __CRANPOSE_DEFINITION_KEY: #core_path::Key =
306 #core_path::hot_definition_key(file!(), module_path!(), #name);
307 __CRANPOSE_DEFINITION_KEY
308 });
309 };
310 }
311 quote! {
312 let #caller_key_ident = #core_path::composable_identity_key({
313 struct __CranposeDefinitionMarker;
314 static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
315 ::std::sync::OnceLock::new();
316 #core_path::cached_composable_definition_key(
317 &__CRANPOSE_DEFINITION_KEY,
318 file!(),
319 line!(),
320 column!(),
321 ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
322 )
323 });
324 }
325}
326
327#[proc_macro_attribute]
339pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
340 let attr_tokens = TokenStream2::from(attr);
341 let mut enable_skip = true;
342 let core_path = core_crate_path();
343 if !attr_tokens.is_empty() {
344 match syn::parse2::<Ident>(attr_tokens) {
345 Ok(ident) if ident == "no_skip" => enable_skip = false,
346 Ok(other) => {
347 return syn::Error::new_spanned(other, "unsupported composable attribute")
348 .to_compile_error()
349 .into();
350 }
351 Err(err) => {
352 return err.to_compile_error().into();
353 }
354 }
355 }
356
357 let mut func = parse_macro_input!(item as ItemFn);
358
359 let mut param_info: Vec<ParamInfo> = Vec::new();
360
361 for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
362 if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
363 if let Some(reserved) = find_reserved_pattern_ident(pat) {
364 let name = reserved.to_string();
365 return syn::Error::new(
366 reserved.span(),
367 format!("`{name}` is reserved by #[composable]"),
368 )
369 .to_compile_error()
370 .into();
371 }
372 let pat_is_mut = matches!(
373 pat.as_ref(),
374 Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
375 );
376 let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
377
378 if is_impl_trait {
379 let ident = if let Pat::Ident(pat_ident) = &**pat {
380 pat_ident.ident.clone()
381 } else {
382 Ident::new(&format!("__arg{index}"), Span::mixed_site())
383 };
384 param_info.push(ParamInfo {
385 ident,
386 pat: pat.clone(),
387 ty: ty.as_ref().clone(),
388 pat_is_mut,
389 is_impl_trait: true,
390 });
391 } else {
392 let ident = Ident::new(&format!("__arg{index}"), Span::mixed_site());
393 let original_pat: Box<Pat> = pat.clone();
394 **pat = syn::parse_quote! { #ident };
395 param_info.push(ParamInfo {
396 ident,
397 pat: original_pat,
398 ty: ty.as_ref().clone(),
399 pat_is_mut,
400 is_impl_trait: false,
401 });
402 }
403 }
404 }
405
406 inject_groups(&core_path, &mut func);
407 let has_rust_abi = match &func.sig.abi {
408 None => true,
409 Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
410 };
411 if has_rust_abi {
412 func.attrs.push(syn::parse_quote!(#[track_caller]));
413 }
414 func.attrs.push(syn::parse_quote!(
415 #[allow(non_snake_case, reason = "names follow Compose's casing")]
416 ));
417
418 let scope_label_ident = func.sig.ident.clone();
419 let original_block = func.block.clone();
420 let body_end = func.block.brace_token.span.close();
421 let composer_ident = Ident::new("__composer", Span::mixed_site());
422 let slot_key = slot_key(&core_path);
423 let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
424 let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
425 let result_ident = Ident::new("__result", Span::mixed_site());
426 let group_ident = Ident::new("__cranpose_group", Span::mixed_site());
427 let key_expr = quote! { #caller_key_ident };
428 let caller_key_stmt = definition_key_stmt(&core_path, &caller_key_ident, &scope_label_ident);
429
430 let rebinds_for_no_skip: Vec<_> = param_info
431 .iter()
432 .map(|info| {
433 let ident = &info.ident;
434 let pat = &info.pat;
435 quote! { let #pat = #ident; }
436 })
437 .collect();
438
439 let return_ty: Type = match &func.sig.output {
440 ReturnType::Default => syn::parse_quote! { () },
441 ReturnType::Type(_, ty) => ty.as_ref().clone(),
442 };
443 let returns_unit = returns_unit(&func.sig.output);
444 let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
445 quote! {}
446 } else {
447 quote! { #composer_ident.__invalidate_return_consumer_scope(); }
448 };
449 let _helper_ident = Ident::new(
450 &format!("__cranpose_impl_{}", func.sig.ident),
451 Span::mixed_site(),
452 );
453 let generics = func.sig.generics.clone();
454 let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
455
456 let _helper_inputs: Vec<TokenStream2> = param_info
457 .iter()
458 .map(|info| {
459 let ident = &info.ident;
460 let ty = &info.ty;
461 quote! { #ident: #ty }
462 })
463 .collect();
464
465 let held: Vec<Option<HeldCallback>> = param_info
466 .iter()
467 .map(|info| HeldCallback::of(&info.ty, &func.sig.generics))
468 .collect();
469 let has_unhandled_impl_trait = param_info
471 .iter()
472 .zip(&held)
473 .any(|(info, held)| (info.is_impl_trait && held.is_none()) || borrows(&info.ty));
474
475 if enable_skip && !has_unhandled_impl_trait {
476 let helper_ident = Ident::new(
477 &format!("__cranpose_impl_{}", func.sig.ident),
478 Span::mixed_site(),
479 );
480 let generics = func.sig.generics.clone();
481
482 let param_state_slots: Vec<Ident> = (0..param_info.len())
483 .map(|index| Ident::new(&format!("__param_state_slot{index}"), Span::mixed_site()))
484 .collect();
485 let callbacks: Vec<Option<CallbackParam>> = param_info
486 .iter()
487 .zip(¶m_state_slots)
488 .zip(&held)
489 .map(|((info, slot), held)| {
490 CallbackParam::of(&core_path, info, slot, held.as_ref(), &generics)
491 })
492 .collect();
493 let param_erased: Vec<bool> = callbacks
494 .iter()
495 .map(|callback| callback.as_ref().is_some_and(CallbackParam::boxes))
496 .collect();
497
498 let mut strippable: std::collections::BTreeSet<String> = param_info
499 .iter()
500 .zip(¶m_erased)
501 .filter(|(info, erased)| **erased && !info.is_impl_trait)
502 .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
503 .map(Ident::to_string)
504 .collect();
505 loop {
506 use quote::ToTokens;
507 let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
508 for (info, erased) in param_info.iter().zip(¶m_erased) {
509 if !*erased {
510 used_elsewhere.push(info.ty.to_token_stream());
511 }
512 }
513 used_elsewhere.push(return_ty.to_token_stream());
514 for param in &generics.params {
515 match param {
516 syn::GenericParam::Type(type_param) => {
517 if !strippable.contains(&type_param.ident.to_string()) {
518 used_elsewhere.push(type_param.bounds.to_token_stream());
519 if let Some((_, default)) = &type_param.default {
520 used_elsewhere.push(default.to_token_stream());
521 }
522 }
523 }
524 syn::GenericParam::Const(const_param) => {
525 used_elsewhere.push(const_param.ty.to_token_stream());
526 }
527 syn::GenericParam::Lifetime(_) => {}
528 }
529 }
530 if let Some(where_clause) = &generics.where_clause {
531 for predicate in &where_clause.predicates {
532 if let syn::WherePredicate::Type(pred) = predicate
533 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
534 && strippable.contains(&ident.to_string())
535 {
536 continue;
537 }
538 used_elsewhere.push(predicate.to_token_stream());
539 }
540 }
541 let before = strippable.len();
542 strippable.retain(|name| {
543 !used_elsewhere
544 .iter()
545 .any(|tokens| stream_mentions_ident(tokens, name))
546 });
547 if strippable.len() == before {
548 break;
549 }
550 }
551
552 let helper_generics = filter_generics(&generics, &strippable);
553 let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
554 let ty_generics_turbofish = ty_generics.as_turbofish();
555
556 let helper_inputs: Vec<TokenStream2> = param_info
557 .iter()
558 .zip(&callbacks)
559 .map(|(info, callback)| {
560 let ident = &info.ident;
561 match callback.as_ref().and_then(CallbackParam::boxed_type) {
562 Some(boxed) => quote! { #ident: ::std::boxed::Box<#boxed> },
563 None => {
564 let ty = &info.ty;
565 quote! { #ident: #ty }
566 }
567 }
568 })
569 .collect();
570
571 let param_setup: Vec<TokenStream2> = callbacks
572 .iter()
573 .flatten()
574 .map(|callback| callback.setup(&core_path, &composer_ident))
575 .collect();
576 let param_setup_recompose: Vec<TokenStream2> = callbacks
577 .iter()
578 .flatten()
579 .map(|callback| callback.recompose_setup(&core_path, &composer_ident))
580 .collect();
581 let callback_rebinds: Vec<TokenStream2> = callbacks
582 .iter()
583 .flatten()
584 .map(|callback| callback.rebind(&core_path, &composer_ident))
585 .collect();
586
587 let body_inputs: Vec<TokenStream2> = param_info
588 .iter()
589 .zip(&callbacks)
590 .map(|(info, callback)| match callback {
591 Some(callback) => callback.body_input(&core_path),
592 None => {
593 let pat = &info.pat;
594 let ty = &info.ty;
595 quote! { #pat: #ty }
596 }
597 })
598 .collect();
599 let body_args: Vec<&Ident> = param_info
600 .iter()
601 .zip(&callbacks)
602 .map(|(info, callback)| {
603 callback
604 .as_ref()
605 .map_or(&info.ident, |callback| callback.slot)
606 })
607 .collect();
608
609 let packed = PackedParams {
610 params: param_info
611 .iter()
612 .zip(&callbacks)
613 .filter(|(info, callback)| callback.is_none() && !info.is_impl_trait)
614 .map(|(info, _)| (&info.ident, &info.ty))
615 .collect(),
616 returns: (!returns_unit).then_some(&return_ty),
617 };
618 let skippable = true;
620 let param_setup = packed.with_setup(
621 &core_path,
622 &composer_ident,
623 (&ReturningCall::previous(), skippable),
624 param_setup,
625 );
626 let param_setup_recompose =
627 packed.with_slot(&core_path, &composer_ident, param_setup_recompose);
628 let reads_for_recompose = packed.reads(&core_path, &composer_ident);
629
630 let body_ident = Ident::new(
631 &format!("__cranpose_body_{}", func.sig.ident),
632 Span::mixed_site(),
633 );
634 let body_call = quote! {
635 #body_ident #ty_generics_turbofish (#composer_ident #(, #body_args)*)
636 };
637
638 let recompose_fn_ident = Ident::new(
639 &format!("__cranpose_recompose_{}", func.sig.ident),
640 Span::mixed_site(),
641 );
642
643 let recompose_fn = quote! { #recompose_fn_ident #ty_generics_turbofish };
644
645 let returning = ReturningCall {
646 core_path: &core_path,
647 composer: &composer_ident,
648 group: &group_ident,
649 current_scope: ¤t_scope_ident,
650 scope_label: &scope_label_ident,
651 return_ty: &return_ty,
652 slot_key: &slot_key,
653 packed: &packed,
654 skippable,
655 body_call: &body_call,
656 };
657 let helper_body = if returns_unit {
658 unit_helper_body(&returning, ¶m_setup, &recompose_fn)
659 } else {
660 let setter =
661 recompose_setter(&group_ident, &recompose_fn, (&core_path, &composer_ident));
662 returning.helper_body(¶m_setup, &setter)
663 };
664
665 let recompose_fn_body = if returns_unit {
666 quote! {
667 #(#param_setup_recompose)*
668 #(#reads_for_recompose)*
669 #body_call;
670 }
671 } else {
672 returning.recompose_body(
673 ¶m_setup_recompose,
674 &reads_for_recompose,
675 &invalidate_return_consumer,
676 )
677 };
678
679 let slot_origin = hot_slot_origin(&core_path, &func.sig.ident, body_end);
680 let recompose_fn = quote! {
681 #[allow(non_snake_case, reason = "names follow Compose's casing")]
682 fn #recompose_fn_ident #impl_generics (
683 #composer_ident: &#core_path::Composer
684 ) -> #return_ty #where_clause {
685 #slot_origin
686 #recompose_fn_body
687 }
688 };
689
690 let helper_fn = quote! {
691 #[allow(non_snake_case, clippy::too_many_arguments, reason = "the helper takes the composable's own parameters")]
692 fn #helper_ident #impl_generics (
693 #composer_ident: &#core_path::Composer,
694 #group_ident: &#core_path::ComposableGroup<'_>
695 #(, #helper_inputs)*
696 ) -> #return_ty #where_clause {
697 #slot_origin
698 let #current_scope_ident = #group_ident.scope();
699 #helper_body
700 }
701 };
702
703 let body_fn = quote! {
704 #[allow(non_snake_case, clippy::too_many_arguments, reason = "the helper takes the composable's own parameters")]
705 fn #body_ident #impl_generics (
706 #composer_ident: &#core_path::Composer
707 #(, #body_inputs)*
708 ) -> #return_ty #where_clause {
709 #(#callback_rebinds)*
710 let _cranpose_source_scope = #core_path::__source_scope(
711 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
712 );
713 #original_block
714 }
715 };
716
717 let wrapper_args: Vec<TokenStream2> = param_info
718 .iter()
719 .zip(¶m_erased)
720 .map(|(info, erased)| {
721 let ident = &info.ident;
722 if *erased {
723 quote! { ::std::boxed::Box::new(#ident) }
724 } else {
725 quote! { #ident }
726 }
727 })
728 .collect();
729
730 let open_group = open_group_fn(returns_unit);
731 let wrapped = quote!({
732 #caller_key_stmt
733 let #composer_ident = #core_path::__current_composer();
734 let #group_ident = #composer_ident.#open_group(#key_expr);
735 let #result_ident = #helper_ident(
736 &#composer_ident,
737 &#group_ident
738 #(, #wrapper_args)*
739 );
740 #group_ident.close();
741 #result_ident
742 });
743 *func.block = syn::parse2(wrapped).expect("failed to build block");
744 TokenStream::from(quote! {
745 #body_fn
746 #recompose_fn
747 #helper_fn
748 #func
749 })
750 } else {
751 let wrapped = quote!({
752 #core_path::with_current_composer(|#composer_ident: &#core_path::Composer| {
753 #(#rebinds_for_no_skip)*
754 let _cranpose_source_scope = #core_path::__inline_source_scope(
755 stringify!(#scope_label_ident), file!(), line!(), env!("CARGO_MANIFEST_DIR"),
756 );
757 #original_block
758 })
759 });
760 *func.block = syn::parse2(wrapped).expect("failed to build block");
761 TokenStream::from(quote! { #func })
762 }
763}
764
765fn inject_groups(core_path: &TokenStream2, func: &mut ItemFn) {
768 let hot_reload = cfg!(feature = "hot-reload");
769 branch_groups::inject_branch_groups_with(
770 core_path,
771 &mut func.block,
772 &func.sig.ident,
773 hot_reload,
774 );
775 if !hot_reload {
778 constant_modifiers::keep_constant_modifiers(core_path, &mut func.block);
779 }
780 if hot_reload {
781 let end = func.block.brace_token.span.close();
782 let origin = branch_groups::hot_origin_stmt(core_path, &func.sig.ident, end);
783 func.block
784 .stmts
785 .insert(0, syn::parse2(origin).expect("hot origin statement"));
786 }
787}
788
789fn returns_unit(output: &ReturnType) -> bool {
791 match output {
792 ReturnType::Default => true,
793 ReturnType::Type(_, ty) => {
794 matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
795 }
796 }
797}
798
799fn recompose_setter(
802 group: &Ident,
803 recompose_fn: &TokenStream2,
804 (core_path, composer): (&TokenStream2, &Ident),
805) -> TokenStream2 {
806 quote! {
807 #group.set_recompose_fn(|#composer: &#core_path::Composer| {
808 let _ = #recompose_fn(#composer);
809 });
810 }
811}
812
813fn unit_helper_body(
819 call: &ReturningCall<'_>,
820 param_setup: &[TokenStream2],
821 recompose_fn: &TokenStream2,
822) -> TokenStream2 {
823 let (core_path, group, current_scope) = (call.core_path, call.group, call.current_scope);
824 let (scope_label, body_call) = (call.scope_label, call.body_call);
825 quote! {
826 #core_path::debug_label_current_scope(stringify!(#scope_label));
827 let mut __changed = #current_scope.should_recompose();
828 #(#param_setup)*
829 if !#group.should_execute(__changed.into(), #recompose_fn) {
830 return;
831 }
832 #body_call
833 }
834}
835
836fn open_group_fn(returns_unit: bool) -> Ident {
839 let name = if returns_unit {
840 "__open_pausable_group"
841 } else {
842 "__open_composable_group"
843 };
844 Ident::new(name, Span::call_site())
845}
846
847fn hot_slot_origin(core_path: &TokenStream2, name: &Ident, end: Span) -> TokenStream2 {
851 if cfg!(feature = "hot-reload") {
852 branch_groups::hot_origin_stmt(core_path, name, end)
853 } else {
854 TokenStream2::new()
855 }
856}
857
858fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
859 use syn::visit::Visit;
860
861 struct Scan<'ast> {
862 found: Option<&'ast Ident>,
863 }
864 impl<'ast> Visit<'ast> for Scan<'ast> {
865 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
866 if self.found.is_none() {
867 let name = node.ident.to_string();
868 if name == "__composer" || name.starts_with("__cranpose") {
869 self.found = Some(&node.ident);
870 }
871 }
872 syn::visit::visit_pat_ident(self, node);
873 }
874 }
875 let mut scan = Scan { found: None };
876 scan.visit_pat(pat);
877 scan.found
878}
879
880#[cfg(test)]
881#[path = "tests/macros_tests.rs"]
882mod tests;