Skip to content

Commit 90744b0

Browse files
cartmockersfurben1680
authored
bsn: support field name shorthand (#24526)
# Objective `bsn!` should support Rust's "name shorthand syntax": ```rust let value = 10; Foo { value } ``` ## Solution Add support, both for normal types and "props": ```rust let value = 10; bsn! { Foo { value } } let value = 10; bsn! { @foo { @value } } ``` This also changes generated internal bsn! variables to use `__X` formatting everywhere to avoid conflicts with user-defined variables (we were already using this strategy in some places ... this just uses it everywhere). ## Testing - added a test --------- Co-authored-by: François Mockers <mockersf@gmail.com> Co-authored-by: urben1680 <55257931+urben1680@users.noreply.github.com>
1 parent fd4f66f commit 90744b0

4 files changed

Lines changed: 113 additions & 33 deletions

File tree

crates/bevy_scene/macros/src/bsn/codegen.rs

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ impl BsnEntry {
205205
let mut assigns = Vec::new();
206206
let target = PatchTarget {
207207
path: &[Member::Named(Ident::new(
208-
"value",
208+
"__value",
209209
proc_macro2::Span::call_site(),
210210
))],
211211
is_ref: true,
@@ -218,7 +218,7 @@ impl BsnEntry {
218218
}
219219
} else {
220220
quote! {
221-
let value = _scene.get_or_insert_template::<#path>(_context);
221+
let __value = _scene.get_or_insert_template::<#path>(_context);
222222
#(#assigns)*
223223
}
224224
})
@@ -227,7 +227,7 @@ impl BsnEntry {
227227
let mut assigns = Vec::new();
228228
let target = PatchTarget {
229229
path: &[Member::Named(Ident::new(
230-
"value",
230+
"__value",
231231
proc_macro2::Span::call_site(),
232232
))],
233233
is_ref: true,
@@ -240,7 +240,7 @@ impl BsnEntry {
240240
}
241241
} else {
242242
quote! {
243-
let value = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);
243+
let __value = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);
244244
#(#assigns)*
245245
}
246246
})
@@ -249,8 +249,8 @@ impl BsnEntry {
249249
type_path,
250250
const_ident,
251251
} => EntryResult::CombinedSceneFunction(quote! {
252-
let value = _scene.get_or_insert_template::<#type_path>(_context);
253-
*value = #type_path::#const_ident;
252+
let __value = _scene.get_or_insert_template::<#type_path>(_context);
253+
*__value = #type_path::#const_ident;
254254
}),
255255
BsnEntry::TemplateConstructor(BsnConstructor {
256256
type_path,
@@ -259,8 +259,8 @@ impl BsnEntry {
259259
}) => EntryResult::CombinedSceneFunction({
260260
let args = args.to_tokens(ctx);
261261
quote! {
262-
let value = _scene.get_or_insert_template::<#type_path>(_context);
263-
*value = #type_path::#function #args;
262+
let __value = _scene.get_or_insert_template::<#type_path>(_context);
263+
*__value = #type_path::#function #args;
264264
}
265265
}),
266266
BsnEntry::FromTemplateConstructor(BsnConstructor {
@@ -270,8 +270,8 @@ impl BsnEntry {
270270
}) => EntryResult::CombinedSceneFunction({
271271
let args = args.to_tokens(ctx);
272272
quote! {
273-
let value = _scene.get_or_insert_template::<<#type_path as #bevy_ecs::template::FromTemplate>::Template>(_context);
274-
*value = <#type_path as #bevy_ecs::template::FromTemplate>::Template::#function #args;
273+
let __value = _scene.get_or_insert_template::<<#type_path as #bevy_ecs::template::FromTemplate>::Template>(_context);
274+
*__value = <#type_path as #bevy_ecs::template::FromTemplate>::Template::#function #args;
275275
}
276276
}),
277277
BsnEntry::RelatedSceneList(BsnRelatedSceneList {
@@ -310,8 +310,8 @@ impl BsnScene {
310310
// which imposes constraints like requiring the type to impl FromTemplate, and requiring
311311
// enums to have VariantDefault.
312312
let mut assignments = Vec::new();
313-
let props = format_ident!("props");
314-
let props_ref = format_ident!("props_ref");
313+
let props = format_ident!("__props");
314+
let props_ref = format_ident!("__props_ref");
315315
let target = PatchTarget {
316316
path: &[Member::Named(props_ref.clone())],
317317
is_ref: true,
@@ -320,7 +320,7 @@ impl BsnScene {
320320
let mut assigns = Vec::new();
321321
let target = PatchTarget {
322322
path: &[Member::Named(Ident::new(
323-
"value",
323+
"__value",
324324
proc_macro2::Span::call_site(),
325325
))],
326326
is_ref: true,
@@ -329,7 +329,7 @@ impl BsnScene {
329329
let path = &bsn_type.path;
330330
let bevy_scene = ctx.bevy_scene;
331331
let from_template_patch = quote! {
332-
<#path as #bevy_scene::PatchFromTemplate>::patch(move |value, _context| {
332+
<#path as #bevy_scene::PatchFromTemplate>::patch(move |__value, _context| {
333333
#(#assigns)*
334334
})
335335
};
@@ -489,15 +489,15 @@ impl BsnType {
489489
continue;
490490
}
491491

492-
if field.value.is_none() {
492+
if field.value.is_none() && !field.is_name_shorthand {
493493
ctx.errors.push(syn::Error::new_spanned(
494494
field_name,
495495
format!("Field `{}` is missing a value.", field_name),
496496
));
497497
}
498498

499499
let path = if field.is_prop {
500-
&[Member::Named(format_ident!("props"))]
500+
&[Member::Named(format_ident!("__props"))]
501501
} else {
502502
target.path
503503
};
@@ -508,6 +508,7 @@ impl BsnType {
508508
path,
509509
Member::Named(field_name.clone()),
510510
field.value.as_ref(),
511+
field.is_name_shorthand,
511512
)?;
512513
}
513514
}
@@ -523,6 +524,7 @@ impl BsnType {
523524
target.path,
524525
Member::Unnamed(Index::from(i)),
525526
Some(&field.value),
527+
false,
526528
) {
527529
ctx.errors.push(err);
528530
}
@@ -539,25 +541,31 @@ impl BsnType {
539541
base_path: &[Member],
540542
member: Member,
541543
value: Option<&BsnValue>,
544+
is_name_shorthand: bool,
542545
) -> syn::Result<()> {
543546
match value {
547+
// NOTE: It is very important to still produce outputs for None field values. This is what
548+
// enables field autocomplete in Rust Analyzer
549+
None => {
550+
if is_name_shorthand {
551+
assignments.push(quote! {
552+
#(#base_path.)*#member = #member.into();
553+
});
554+
} else {
555+
assignments.push(quote! {
556+
#(#base_path.)*#member;
557+
});
558+
}
559+
}
544560
// Enables field autocomplete in Rust Analyzer
545561
Some(
546-
BsnValue::Ident(_) | BsnValue::Expr(_) | BsnValue::Closure(_) | BsnValue::Tuple(_),
547-
)
548-
| None => {
549-
// NOTE: It is very important to still produce outputs for None field values. This is what
550-
// enables field autocomplete in Rust Analyzer
551-
assignments.push(
552-
value
553-
.map(|v| {
554-
let ident = ctx.hoisted_expressions.hoist(v);
555-
quote! { #(#base_path.)*#member = #ident; }
556-
})
557-
.unwrap_or(quote! {
558-
#(#base_path.)*#member;
559-
}),
560-
);
562+
value @ (BsnValue::Ident(_)
563+
| BsnValue::Expr(_)
564+
| BsnValue::Closure(_)
565+
| BsnValue::Tuple(_)),
566+
) => {
567+
let ident = ctx.hoisted_expressions.hoist(value);
568+
assignments.push(quote! { #(#base_path.)*#member = #ident; });
561569
}
562570
Some(BsnValue::Lit(_)) => {
563571
// value is Some
@@ -781,11 +789,13 @@ mod tests {
781789
name: parse_quote!(x),
782790
value: Some(BsnValue::Expr(quote!({}))),
783791
is_prop: false,
792+
is_name_shorthand: false,
784793
},
785794
BsnNamedField {
786795
name: parse_quote!(x),
787796
value: Some(BsnValue::Expr(quote!({}))),
788797
is_prop: false,
798+
is_name_shorthand: false,
789799
},
790800
]),
791801
};
@@ -820,6 +830,7 @@ mod tests {
820830
enum_variant: None,
821831
fields: BsnFields::Named(vec![BsnNamedField {
822832
is_prop: false,
833+
is_name_shorthand: false,
823834
name: parse_quote!(child_field),
824835
value: Some(BsnValue::Type(BsnType {
825836
path: parse_quote!(Child),
@@ -829,11 +840,13 @@ mod tests {
829840
name: parse_quote!(x),
830841
value: Some(BsnValue::Expr(quote!({}))),
831842
is_prop: false,
843+
is_name_shorthand: false,
832844
},
833845
BsnNamedField {
834846
name: parse_quote!(x),
835847
value: Some(BsnValue::Expr(quote!({}))),
836848
is_prop: false,
849+
is_name_shorthand: false,
837850
},
838851
]),
839852
})),
@@ -871,6 +884,7 @@ mod tests {
871884
enum_variant: None,
872885
fields: BsnFields::Named(vec![BsnNamedField {
873886
is_prop: false,
887+
is_name_shorthand: false,
874888
name: parse_quote!(x),
875889
value: None,
876890
}]),
@@ -908,11 +922,13 @@ mod tests {
908922
fields: BsnFields::Named(vec![
909923
BsnNamedField {
910924
is_prop: false,
925+
is_name_shorthand: false,
911926
name: parse_quote!(x),
912927
value: Some(BsnValue::Expr(quote!(1))),
913928
},
914929
BsnNamedField {
915930
is_prop: false,
931+
is_name_shorthand: false,
916932
name: parse_quote!(x),
917933
value: Some(BsnValue::Expr(quote!(2))),
918934
},

crates/bevy_scene/macros/src/bsn/parse.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ impl Parse for BsnNamedField {
358358
false
359359
};
360360
let name = input.parse::<Ident>()?;
361+
let mut is_name_shorthand = false;
361362
let value = if input.peek(Colon) {
362363
input.parse::<Colon>()?;
363364

@@ -367,12 +368,14 @@ impl Parse for BsnNamedField {
367368
Some(input.parse::<BsnValue>()?)
368369
}
369370
} else {
371+
is_name_shorthand = true;
370372
None
371373
};
372374
Ok(BsnNamedField {
373375
name,
374376
value,
375377
is_prop,
378+
is_name_shorthand,
376379
})
377380
}
378381
}

crates/bevy_scene/macros/src/bsn/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pub struct BsnTuple(pub Vec<BsnValue>);
9191
#[derive(Debug)]
9292
pub struct BsnNamedField {
9393
pub is_prop: bool,
94+
/// This is a `Struct { field }` shorthand for `Struct { field: field }`
95+
pub is_name_shorthand: bool,
9496
pub name: Ident,
9597
/// This is an Option to enable autocomplete when the field name is being typed
9698
/// To improve autocomplete further we'll need to forgo a lot of the syn parsing

crates/bevy_scene/src/lib.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ use bevy_ecs::prelude::*;
959959
/// | `CompA(val)`<br>`CompA(val, val)` | Tuple Component with some fields specified. Unspecified fields will be default, see [patching](self#patching) |
960960
/// | `CompA { name: val }` | Component with some fields specified. Unspecified fields will be default, see [patching](self#patching) |
961961
/// | `mymodule::CompA { name: val }` | Same as above, but referring to the component by module path |
962+
/// | `CompA { name }` | Component with Rust's "field assignment shorthand". Evaluates to `CompA { name: name.into() }` |
962963
/// | `MyEnum::Variant` | Enum Component `MyEnum` with the `Variant` variant |
963964
/// | `template_value(component)` | Insert the component value from a variable `component` |
964965
/// | `template_value(CompA::from_str("foo"))` | Insert the component value by immediately calling the constructor |
@@ -2837,11 +2838,69 @@ mod tests {
28372838
let entity = world
28382839
.spawn_scene(bsn! {
28392840
Foo {
2840-
value: vec! [ 10usize ],
2841+
value: vec! [ 10 ],
28412842
}
28422843
})
28432844
.unwrap();
2844-
assert_eq!(entity.get::<Foo>().unwrap().value, vec![10usize]);
2845+
assert_eq!(entity.get::<Foo>().unwrap().value, vec![10]);
2846+
}
2847+
2848+
#[test]
2849+
fn field_name_shorthand() {
2850+
let mut app = test_app();
2851+
let world = app.world_mut();
2852+
2853+
#[derive(Component, Default, Clone)]
2854+
struct Foo {
2855+
value: usize,
2856+
}
2857+
let value = 10usize;
2858+
let entity = world.spawn_scene(bsn! { Foo { value } }).unwrap();
2859+
assert_eq!(entity.get::<Foo>().unwrap().value, 10);
2860+
2861+
#[derive(SceneComponent, Default, Clone)]
2862+
#[scene(BarProps)]
2863+
struct Bar {
2864+
value: usize,
2865+
}
2866+
2867+
#[derive(Default)]
2868+
struct BarProps {
2869+
value: usize,
2870+
}
2871+
2872+
impl Bar {
2873+
fn scene(props: BarProps) -> impl Scene {
2874+
bsn! {Bar {
2875+
value: {props.value}
2876+
}}
2877+
}
2878+
}
2879+
2880+
let value = 10usize;
2881+
let entity = world.spawn_scene(bsn! { @Bar { @value } }).unwrap();
2882+
assert_eq!(entity.get::<Bar>().unwrap().value, 10);
2883+
2884+
#[derive(Component, Default, Clone)]
2885+
struct Baz {
2886+
value: X,
2887+
}
2888+
2889+
#[derive(Default, Clone)]
2890+
struct X;
2891+
2892+
#[derive(Default, Clone)]
2893+
struct Y;
2894+
2895+
impl From<Y> for X {
2896+
fn from(_: Y) -> Self {
2897+
X
2898+
}
2899+
}
2900+
2901+
let value = Y;
2902+
// ensure implicit Into works
2903+
let _ = world.spawn_scene(bsn! { Baz { value } }).unwrap();
28452904
}
28462905

28472906
#[test]

0 commit comments

Comments
 (0)