diff --git a/crates/bevy_scene/macros/src/bsn/codegen.rs b/crates/bevy_scene/macros/src/bsn/codegen.rs index e8c7af79a938e..3544aac92e711 100644 --- a/crates/bevy_scene/macros/src/bsn/codegen.rs +++ b/crates/bevy_scene/macros/src/bsn/codegen.rs @@ -633,6 +633,17 @@ impl BsnType { return Ok(quote! {#(#type_assigns)*}); } + if let Some( + value @ (BsnValue::Ident(_) + | BsnValue::Expr(_) + | BsnValue::Closure(_) + | BsnValue::Tuple(_)), + ) = value + { + let ident = ctx.hoisted_expressions.hoist(value); + return Ok(quote! { *#bind_name = #ident; }); + } + // NOTE: It is very important to still produce outputs for None field values. This is what // enables field autocomplete in Rust Analyzer value @@ -995,6 +1006,49 @@ mod tests { .contains("Duplicate field `x` found in BSN enum variant")); } + #[test] + fn enum_variant_expr_is_hoisted() { + let mut refs = EntityRefs::default(); + let paths = TestPaths::new(); + let mut exprs = HoistedExpressions::default(); + let mut ctx = paths.ctx(&mut refs, &mut exprs); + let mut assignments = vec![]; + let handle = BsnType { + path: parse_quote!(FontSourceTemplate), + enum_variant: Some(parse_quote!(Handle)), + fields: BsnFields::Tuple(vec![BsnUnnamedField { + value: BsnValue::Expr(quote!(some_borrow.clone())), + }]), + }; + + let res = handle.push_enum_patch( + &mut ctx, + &parse_quote!(Handle), + &mut assignments, + PatchTarget { + path: &[], + is_ref: false, + }, + ); + + assert!(res.is_ok()); + assert_eq!(ctx.errors.len(), 0); + assert_eq!(exprs.expressions.len(), 1); + assert_eq!( + exprs.expressions[0].to_string(), + "let _expr0 = { some_borrow . clone () } . into () ;" + ); + let assignment_output: String = assignments.iter().map(|t| t.to_string()).collect(); + assert!( + assignment_output.contains("_expr0"), + "expected hoisted ident in assignment output: {assignment_output}" + ); + assert!( + !assignment_output.contains("some_borrow"), + "borrow should not appear inline in assignment output: {assignment_output}" + ); + } + #[test] fn bsn_root_preserves_inference_on_error() { // Arrange diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 2d2011cb773cb..46d4361da570f 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -2881,6 +2881,48 @@ mod tests { assert_eq!(entity.get::().unwrap().font_size, FontSize(24)); } + #[test] + fn enum_variant_subexpressions_are_hoisted() { + #[derive(Component, FromTemplate, PartialEq, Eq, Debug, Clone)] + enum FontSource { + #[default] + Handle { value: String }, + } + + struct Config { + value: String, + } + + fn make_scene(config: &Config) -> impl Scene { + bsn! { + Children [ + (FontSource::Handle { value: { config.value.clone() } }), + (FontSource::Handle { value: { config.value.clone() } }), + ] + } + } + + let mut app = test_app(); + let world = app.world_mut(); + let config = Config { + value: "test".to_string(), + }; + let entity = world.spawn_scene(make_scene(&config)).unwrap().id(); + let children = world.entity(entity).get::().unwrap(); + assert_eq!( + world.entity(children[0]).get::().unwrap(), + &FontSource::Handle { + value: "test".to_string(), + } + ); + assert_eq!( + world.entity(children[1]).get::().unwrap(), + &FontSource::Handle { + value: "test".to_string(), + } + ); + } + #[test] fn field_name_shorthand() { let mut app = test_app();