Path: blob/main/crates/bevy_scene/macros/src/bsn/codegen.rs
30638 views
use crate::bsn::types::{1Bsn, BsnConstructor, BsnEntry, BsnFields, BsnListRoot, BsnRelatedSceneList, BsnRoot, BsnScene,2BsnSceneFn, BsnSceneFnArg, BsnSceneFnArgs, BsnSceneListItem, BsnSceneListItems, BsnType,3BsnValue,4};5use bevy_macro_utils::{fq_std::FQDefault, path_to_string};6use proc_macro2::TokenStream;7use quote::{format_ident, quote, ToTokens};8use std::collections::{hash_map::Entry, HashMap, HashSet};9use syn::{parse::Parse, punctuated::Punctuated, ExprTuple, Ident, Index, Lit, Member, Path};1011/// Tracks named entity references and assigns them unique, sequential indices12/// during the code generation process.13#[derive(Default)]14pub(crate) struct EntityRefs {15refs: HashMap<String, usize>,16next: usize,17}1819impl EntityRefs {20/// Retrieves the index for a given entity name.21/// Creates a new one if it hasn't been seen yet.22fn get(&mut self, name: String) -> usize {23match self.refs.entry(name) {24Entry::Occupied(entry) => *entry.get(),25Entry::Vacant(entry) => {26let index = self.next;27entry.insert(index);28self.next += 1;29index30}31}32}33}3435#[derive(Default)]36pub(crate) struct HoistedExpressions {37expressions: Vec<TokenStream>,38index: usize,39}4041impl HoistedExpressions {42fn next_ident(&mut self) -> Ident {43let index = self.index;44let ident = format_ident!("_expr{index}");45self.index += 1;46ident47}4849pub fn hoist(&mut self, value: &BsnValue) -> Ident {50let ident = self.next_ident();51self.expressions.push(quote! {let #ident = #value;});52ident53}54}5556/// Context used in the [`Bsn`] code generation pipeline.57/// Used to accumulate validation errors without short-circuiting.58pub(crate) struct BsnCodegenCtx<'a> {59pub bevy_scene: &'a Path,60pub bevy_ecs: &'a Path,61pub invocation_index: ExprTuple,62pub entity_refs: &'a mut EntityRefs,63pub hoisted_expressions: &'a mut HoistedExpressions,64/// Accumulated parsing and validation errors.65pub errors: Vec<syn::Error>,66}6768/// Represents the target path and whether it is a reference, e.g.,69/// when applying a template patch.70struct PatchTarget<'a> {71/// The path to the field being patched.72pub path: &'a [Member],73/// Whether the target is a reference.74/// - `true`: Requires dereferencing (`*`) to assign a value to the target.75/// - `false`: Requires a mutable borrow (`&mut`) to create a temporary76/// reference.77pub is_ref: bool,78}7980pub trait BsnTokenStream: Parse {81fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream;82}8384impl BsnTokenStream for BsnRoot {85fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {86let tokens = self.0.to_tokens(ctx);87let errors = ctx.errors.iter().map(|e| e.to_compile_error());88let bevy_scene = ctx.bevy_scene;89let hoisted_exprs = ctx.hoisted_expressions.expressions.drain(..);9091// NOTE: Assigning the result to a variable first so that the LSP's92// type inference can see assignments before it encounters93// any compile errors. This keeps autocomplete working in broken states,94// e.g. when typing the name of a field but no value yet.95quote! {96#bevy_scene::SceneScope({97#(#hoisted_exprs)*98let _res = #tokens;99#(#errors)*100_res101})102}103}104}105106impl BsnTokenStream for BsnListRoot {107fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {108let tokens = self.0.to_tokens(ctx);109let errors = ctx.errors.iter().map(|e| e.to_compile_error());110let bevy_scene = ctx.bevy_scene;111let hoisted_exprs = ctx.hoisted_expressions.expressions.drain(..);112113// NOTE: Assigning the result to a variable first so that the LSP's114// type inference can see assignments before it encounters115// any compile errors. This keeps autocomplete working in broken states,116// e.g. when typing the name of a field but no value yet.117quote! {118{119#(#hoisted_exprs)*120let _res = #bevy_scene::SceneListScope(#tokens);121#(#errors)*122_res123}124}125}126}127128impl<const ALLOW_FLAT: bool> Bsn<ALLOW_FLAT> {129/// Converts to tokens and performs validation checks.130/// Accumulates errors in [`BsnCodegenCtx`].131pub fn try_to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<TokenStream> {132let bevy_scene = ctx.bevy_scene;133let mut combined_patches = Vec::new();134let mut scene_impls = Vec::new();135for entry in &self.entries {136match entry.try_to_tokens(ctx) {137Ok(EntryResult::CombinedSceneFunction(patch)) => combined_patches.push(patch),138Ok(EntryResult::NewSceneImpl(scene_impl)) => {139if !combined_patches.is_empty() {140let patches = combined_patches.drain(..);141scene_impls.push(quote! {142#bevy_scene::SceneFunction(move |_context, _scene| {143#(#patches)*144})145});146}147scene_impls.push(scene_impl)148}149Err(err) => scene_impls.push(err.to_compile_error()),150}151}152if !combined_patches.is_empty() {153let patches = combined_patches.drain(..);154scene_impls.push(quote! {155#bevy_scene::SceneFunction(move |_context, _scene| {156#(#patches)*157})158});159}160Ok(quote! { #bevy_scene::auto_nest_tuple!(#(#scene_impls),*) })161}162163pub fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {164self.try_to_tokens(ctx)165.unwrap_or_else(|e| e.to_compile_error())166}167}168169enum EntryResult {170CombinedSceneFunction(TokenStream),171NewSceneImpl(TokenStream),172}173174impl BsnEntry {175fn try_to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<EntryResult> {176let (bevy_scene, bevy_ecs) = (ctx.bevy_scene, ctx.bevy_ecs);177178Ok(match self {179BsnEntry::TemplatePatch(ty) => {180let mut assigns = Vec::new();181let target = PatchTarget {182path: &[Member::Named(Ident::new(183"value",184proc_macro2::Span::call_site(),185))],186is_ref: true,187};188ty.to_patch_tokens(ctx, &mut assigns, true, false, true, target)?;189let path = &ty.path;190EntryResult::CombinedSceneFunction(if assigns.is_empty() {191quote! {192let _ = _scene.get_or_insert_template::<#path>(_context);193}194} else {195quote! {196let value = _scene.get_or_insert_template::<#path>(_context);197#(#assigns)*198}199})200}201BsnEntry::FromTemplatePatch(ty) => {202let mut assigns = Vec::new();203let target = PatchTarget {204path: &[Member::Named(Ident::new(205"value",206proc_macro2::Span::call_site(),207))],208is_ref: true,209};210ty.to_patch_tokens(ctx, &mut assigns, true, false, false, target)?;211let path = &ty.path;212EntryResult::CombinedSceneFunction(if assigns.is_empty() {213quote! {214let _ = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);215}216} else {217quote! {218let value = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);219#(#assigns)*220}221})222}223BsnEntry::TemplateConst {224type_path,225const_ident,226} => EntryResult::CombinedSceneFunction(quote! {227let value = _scene.get_or_insert_template::<#type_path>(_context);228*value = #type_path::#const_ident;229}),230BsnEntry::TemplateConstructor(BsnConstructor {231type_path,232function,233args,234}) => EntryResult::CombinedSceneFunction({235let args = args.to_tokens(ctx);236quote! {237let value = _scene.get_or_insert_template::<#type_path>(_context);238*value = #type_path::#function(#args);239}240}),241BsnEntry::FromTemplateConstructor(BsnConstructor {242type_path,243function,244args,245}) => EntryResult::CombinedSceneFunction({246let args = args.to_tokens(ctx);247quote! {248let value = _scene.get_or_insert_template::<<#type_path as #bevy_ecs::template::FromTemplate>::Template>(_context);249*value = <#type_path as #bevy_ecs::template::FromTemplate>::Template::#function(#args);250}251}),252BsnEntry::RelatedSceneList(BsnRelatedSceneList {253scene_list,254relationship_path,255}) => {256let scenes = scene_list.0.to_tokens(ctx);257EntryResult::NewSceneImpl(quote! {258#bevy_scene::RelatedScenes::<<#relationship_path as #bevy_ecs::relationship::RelationshipTarget>259::Relationship, _>::new(#scenes)260})261}262BsnEntry::UncachedScene(s) => EntryResult::NewSceneImpl(s.to_tokens(ctx)?),263BsnEntry::CachedScene(s) => EntryResult::NewSceneImpl(s.to_tokens(ctx)?),264BsnEntry::Name(ident) => {265let (name, index) = (ident.to_string(), ctx.entity_refs.get(ident.to_string()));266let invocation = ctx.invocation_index.clone();267EntryResult::CombinedSceneFunction(quote! {268#bevy_scene::NameEntityReference { name: #bevy_ecs::name::Name(#name.into()), reference: #bevy_ecs::template::SceneEntityReference::new(#invocation, #index) }.resolve_inline(_context, _scene);269})270}271BsnEntry::NameExpression(expr) => EntryResult::CombinedSceneFunction(quote! {272let value = _scene.get_or_insert_template::<<#bevy_ecs::name::Name as #bevy_ecs::template::FromTemplate>::Template>(_context);273*value = #bevy_ecs::name::Name({#expr}.into());274}),275})276}277}278279impl BsnScene {280fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<TokenStream> {281let bevy_scene = ctx.bevy_scene;282match self {283BsnScene::Asset(lit) => Ok(quote! {284#bevy_scene::CachedSceneAsset::from(#lit)285}),286BsnScene::Fn(func) => Ok(func.to_tokens(ctx)),287BsnScene::SceneComponent(bsn_type) => {288// TODO: this can and should use a simpler codegen path than BsnType::to_patch_tokens,289// which imposes constraints like requiring the type to impl FromTemplate, and requiring290// enums to have VariantDefault.291let mut assignments = Vec::new();292let props = format_ident!("props");293let props_ref = format_ident!("props_ref");294let target = PatchTarget {295path: &[Member::Named(props_ref.clone())],296is_ref: true,297};298bsn_type.to_patch_tokens(ctx, &mut assignments, false, true, true, target)?;299let mut assigns = Vec::new();300let target = PatchTarget {301path: &[Member::Named(Ident::new(302"value",303proc_macro2::Span::call_site(),304))],305is_ref: true,306};307bsn_type.to_patch_tokens(ctx, &mut assigns, true, false, true, target)?;308let path = &bsn_type.path;309let bevy_scene = ctx.bevy_scene;310let from_template_patch = quote! {311<#path as #bevy_scene::PatchFromTemplate>::patch(move |value, _context| {312#(#assigns)*313})314};315Ok(quote! {{316let mut #props = <<#path as #bevy_scene::SceneComponent>::Props as #FQDefault>::default();317let #props_ref = &mut #props;318#(#assignments)*319(<#path as #bevy_scene::SceneComponent>::scene(#props), #from_template_patch)320}})321}322BsnScene::Expression(tokens) => Ok(quote! {323#tokens324}),325}326}327}328329impl BsnType {330/// Recursively generates token streams.331fn to_patch_tokens(332&self,333ctx: &mut BsnCodegenCtx,334assignments: &mut Vec<TokenStream>,335is_root: bool,336is_props: bool,337is_scene_component: bool,338target: PatchTarget,339) -> syn::Result<()> {340if !is_root {341let (path, bevy_scene) = (&self.path, ctx.bevy_scene);342assignments.push(quote! {#bevy_scene::macro_utils::touch_type::<#path>();});343}344345if let Some(variant) = &self.enum_variant {346if is_props {347self.push_struct_patch(ctx, assignments, true, is_scene_component, target)?;348} else {349self.push_enum_patch(ctx, variant, assignments, target)?;350}351} else {352self.push_struct_patch(ctx, assignments, is_props, is_scene_component, target)?;353}354355Ok(())356}357358fn push_enum_patch(359&self,360ctx: &mut BsnCodegenCtx,361variant: &Ident,362assignments: &mut Vec<TokenStream>,363target: PatchTarget,364) -> syn::Result<()> {365let (bevy_scene, bevy_ecs, path) = (ctx.bevy_scene, ctx.bevy_ecs, &self.path);366let variant_default = format_ident!("default_{}", variant.to_string().to_lowercase());367let template_path = quote! { #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template> };368369let maybe_deref = target.is_ref.then(|| quote! {*});370let maybe_borrow_mut = (!target.is_ref).then(|| quote! {&mut});371let field_path = target.path;372373let (check_pattern, binding_pattern, field_updates) = match &self.fields {374BsnFields::Named(fields) => {375let mut seen = HashSet::with_capacity(fields.len());376let mut names = Vec::new();377let mut assigns = Vec::new();378379for field in fields {380let field_name = &field.name;381if !seen.insert(field_name.to_string()) {382ctx.errors.push(syn::Error::new_spanned(383field_name,384format!("Duplicate field `{}` found in BSN enum variant", field_name),385));386continue;387}388389names.push(field_name);390391assigns.push(self.process_enum_field(ctx, field_name, field.value.as_ref())?);392}393394(395quote! { #variant { .. } },396quote! { #variant { #(#names,)* .. } },397assigns,398)399}400BsnFields::Tuple(fields) if fields.is_empty() => {401(quote! { #variant }, quote! { #variant }, vec![])402}403BsnFields::Tuple(fields) => {404let names: Vec<_> = (0..fields.len()).map(|i| format_ident!("t{}", i)).collect();405let assigns = fields406.iter()407.enumerate()408.map(|(i, f)| self.process_enum_field(ctx, &names[i], Some(&f.value)))409.collect::<syn::Result<Vec<_>>>()?;410411(412quote! { #variant(..) },413quote! { #variant(#(#names,)* ..) },414assigns,415)416}417};418419assignments.push(quote! {420{421let _node = #maybe_borrow_mut #(#field_path).*;422if !::core::matches!(_node, #template_path::#check_pattern) {423#maybe_deref _node = #template_path::#variant_default();424}425if let #template_path::#binding_pattern = _node {426#(#field_updates)*427}428}429});430Ok(())431}432433fn push_struct_patch(434&self,435ctx: &mut BsnCodegenCtx,436assignments: &mut Vec<TokenStream>,437is_props: bool,438is_scene_component: bool,439target: PatchTarget,440) -> syn::Result<()> {441match &self.fields {442BsnFields::Named(fields) => {443let mut seen = HashSet::with_capacity(fields.len());444445for field in fields {446let field_name = &field.name;447if is_props != field.is_prop {448if !is_scene_component && field.is_prop {449let type_path = &self.path;450ctx.errors.push(syn::Error::new_spanned(451field_name,452format!(453"Scene prop fields are not supported in normal component patches\454. If you would like to set a component scene's prop field, it \455should be set using \"scene component\" syntax: \456bsn! {{ @{} {{ @{field_name}: VALUE }} }}",457path_to_string(type_path)458),459));460}461continue;462}463if !seen.insert(field_name.to_string()) {464ctx.errors.push(syn::Error::new_spanned(465field_name,466format!("Duplicate field `{}` found in BSN struct", field_name),467));468continue;469}470471if field.value.is_none() {472ctx.errors.push(syn::Error::new_spanned(473field_name,474format!("Field `{}` is missing a value.", field_name),475));476}477478let path = if field.is_prop {479&[Member::Named(format_ident!("props"))]480} else {481target.path482};483484self.process_field(485ctx,486assignments,487path,488Member::Named(field_name.clone()),489field.value.as_ref(),490)?;491}492}493BsnFields::Tuple(fields) => {494// Tuple fields can't be props495if is_props {496return Ok(());497}498for (i, field) in fields.iter().enumerate() {499if let Err(err) = self.process_field(500ctx,501assignments,502target.path,503Member::Unnamed(Index::from(i)),504Some(&field.value),505) {506ctx.errors.push(err);507}508}509}510}511Ok(())512}513514fn process_field(515&self,516ctx: &mut BsnCodegenCtx,517assignments: &mut Vec<TokenStream>,518base_path: &[Member],519member: Member,520value: Option<&BsnValue>,521) -> syn::Result<()> {522match value {523// Enables field autocomplete in Rust Analyzer524Some(525BsnValue::Ident(_) | BsnValue::Expr(_) | BsnValue::Closure(_) | BsnValue::Tuple(_),526)527| None => {528// NOTE: It is very important to still produce outputs for None field values. This is what529// enables field autocomplete in Rust Analyzer530assignments.push(531value532.map(|v| {533let ident = ctx.hoisted_expressions.hoist(v);534quote! { #(#base_path.)*#member = #ident; }535})536.unwrap_or(quote! {537#(#base_path.)*#member;538}),539);540}541Some(BsnValue::Lit(_)) => {542// value is Some543let value = value.unwrap();544assignments.push(quote! { #(#base_path.)*#member = #value; });545}546Some(BsnValue::Name(ident)) => {547let index = ctx.entity_refs.get(ident.to_string());548let bevy_ecs = ctx.bevy_ecs;549let invocation = ctx.invocation_index.clone();550assignments.push(quote! {551#(#base_path.)*#member = #bevy_ecs::template::EntityTemplate::SceneEntityReference(#bevy_ecs::template::SceneEntityReference::new(#invocation, #index));552});553}554Some(BsnValue::NameExpression(tokens)) => {555assignments.push(quote! {556#(#base_path.)*#member = #tokens.into();557});558}559Some(BsnValue::Type(ty)) if ty.enum_variant.is_some() => {560assignments.push(quote! {#(#base_path.)*#member = #ty;});561}562Some(BsnValue::Type(ty)) => {563let mut new_path = base_path.to_vec();564new_path.push(member);565ty.to_patch_tokens(566ctx,567assignments,568false,569false,570false,571PatchTarget {572path: &new_path,573is_ref: false,574},575)?;576}577}578Ok(())579}580581fn process_enum_field(582&self,583ctx: &mut BsnCodegenCtx,584bind_name: &Ident,585value: Option<&BsnValue>,586) -> syn::Result<TokenStream> {587if value.is_none() {588ctx.errors.push(syn::Error::new_spanned(589bind_name,590format!("Enum field `{}` is missing a value", bind_name),591));592}593594if let Some(BsnValue::Type(ty)) = value595&& ty.enum_variant.is_none()596{597let mut type_assigns = Vec::new();598ty.to_patch_tokens(599ctx,600&mut type_assigns,601false,602false,603false,604PatchTarget {605path: &[Member::Named(bind_name.clone())],606is_ref: true,607},608)?;609return Ok(quote! {#(#type_assigns)*});610}611612// NOTE: It is very important to still produce outputs for None field values. This is what613// enables field autocomplete in Rust Analyzer614value615.map(|v| Ok(quote! { *#bind_name = #v; }))616.unwrap_or(Ok(quote! { #bind_name; }))617}618}619620impl BsnTokenStream for BsnSceneListItems {621fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {622let bevy_scene = ctx.bevy_scene;623let scenes = self.0.iter().map(|s| match s {624BsnSceneListItem::Scene(bsn) => {625let tokens = bsn.to_tokens(ctx);626quote! {#bevy_scene::EntityScene(#tokens)}627}628BsnSceneListItem::Expression(stmts) => quote! {#(#stmts)*},629});630631quote! { #bevy_scene::auto_nest_tuple!(#(#scenes),*) }632}633}634635impl BsnTokenStream for BsnSceneFnArg {636fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {637let bevy_ecs = ctx.bevy_ecs;638match self {639BsnSceneFnArg::Expr(expr) => quote! {#expr},640BsnSceneFnArg::NameExpression(tokens) => {641quote! {#bevy_ecs::template::EntityTemplate::Entity(#tokens)}642}643BsnSceneFnArg::Name(ident) => {644let index = ctx.entity_refs.get(ident.to_string());645let invocation = ctx.invocation_index.clone();646quote! {647#bevy_ecs::template::EntityTemplate::SceneEntityReference(648#bevy_ecs::template::SceneEntityReference::new(#invocation, #index)649)650}651}652}653}654}655656impl BsnTokenStream for BsnSceneFnArgs {657fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {658let mut args: Punctuated<_, syn::Token![,]> = Punctuated::new();659// rebuilding the punctuated is required because ctx needs to be passed660for arg in self.0.iter().flatten() {661args.push(arg.to_tokens(ctx));662}663quote! {#args}664}665}666impl BsnSceneFn {667fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {668let bevy_scene = ctx.bevy_scene;669let args = self.args.to_tokens(ctx);670let path = self.path.clone();671quote! {#bevy_scene::SceneScope(#path(#args))}672}673}674675impl ToTokens for BsnType {676fn to_tokens(&self, tokens: &mut TokenStream) {677let (path, variant) = (678&self.path,679self.enum_variant.as_ref().map(|v| quote! {::#v}),680);681match &self.fields {682BsnFields::Named(fields) => {683let assigns = fields.iter().map(|f| {684let (name, value) = (&f.name, &f.value);685quote! {#name: #value}686});687quote! { #path #variant { #(#assigns,)* } }688}689BsnFields::Tuple(fields) => {690let assigns = fields.iter().map(|f| &f.value);691quote! { #path #variant ( #(#assigns,)* ) }692}693}694.to_tokens(tokens);695}696}697698impl ToTokens for BsnValue {699fn to_tokens(&self, tokens: &mut TokenStream) {700match self {701BsnValue::Expr(e) => quote! {{#e}.into()}.to_tokens(tokens),702BsnValue::Closure(c) => quote! {(#c).into()}.to_tokens(tokens),703BsnValue::Ident(i) => quote! {(#i).into()}.to_tokens(tokens),704BsnValue::Lit(Lit::Str(s)) => quote! {#s.into()}.to_tokens(tokens),705BsnValue::Lit(l) => {706if l.suffix().is_empty() {707l.to_tokens(tokens)708} else {709quote! {(#l).into()}.to_tokens(tokens)710}711}712BsnValue::Tuple(t) => {713let inner = t.0.iter();714quote! {(#(#inner),*)}.to_tokens(tokens);715}716BsnValue::Type(ty) => ty.to_tokens(tokens),717BsnValue::Name(_) | BsnValue::NameExpression(_) => {718// Name requires additional context to convert to tokens719unreachable!()720}721}722}723}724725#[cfg(test)]726mod tests {727use super::*;728use crate::bsn::types::*;729use syn::parse_quote;730731struct TestPaths {732bevy_scene: Path,733bevy_ecs: Path,734}735736impl TestPaths {737fn new() -> Self {738Self {739bevy_scene: parse_quote!(bevy_scene),740bevy_ecs: parse_quote!(bevy_ecs),741}742}743744fn ctx<'a>(745&'a self,746refs: &'a mut EntityRefs,747hoisted_expressions: &'a mut HoistedExpressions,748) -> BsnCodegenCtx<'a> {749BsnCodegenCtx {750bevy_scene: &self.bevy_scene,751bevy_ecs: &self.bevy_ecs,752entity_refs: refs,753invocation_index: parse_quote!(("", 0, 0)),754hoisted_expressions,755errors: Vec::new(),756}757}758}759760#[test]761fn duplicate_field() {762let mut refs = EntityRefs::default();763let paths = TestPaths::new();764let mut exprs = HoistedExpressions::default();765let mut ctx = paths.ctx(&mut refs, &mut exprs);766let mut assignments = vec![];767let duplicate = BsnType {768path: parse_quote!(Transform),769enum_variant: None,770fields: BsnFields::Named(vec![771BsnNamedField {772name: parse_quote!(x),773value: Some(BsnValue::Expr(quote!({}))),774is_prop: false,775},776BsnNamedField {777name: parse_quote!(x),778value: Some(BsnValue::Expr(quote!({}))),779is_prop: false,780},781]),782};783784let res = duplicate.push_struct_patch(785&mut ctx,786&mut assignments,787false,788false,789PatchTarget {790path: &[],791is_ref: false,792},793);794795assert!(res.is_ok());796assert_eq!(ctx.errors.len(), 1);797assert!(ctx.errors[0]798.to_string()799.contains("Duplicate field `x` found in BSN struct"));800}801802#[test]803fn recursive_duplicate_field() {804let mut refs = EntityRefs::default();805let paths = TestPaths::new();806let mut exprs = HoistedExpressions::default();807let mut ctx = paths.ctx(&mut refs, &mut exprs);808let mut assignments = vec![];809let nested_duplicate = BsnType {810path: parse_quote!(Parent),811enum_variant: None,812fields: BsnFields::Named(vec![BsnNamedField {813is_prop: false,814name: parse_quote!(child_field),815value: Some(BsnValue::Type(BsnType {816path: parse_quote!(Child),817enum_variant: None,818fields: BsnFields::Named(vec![819BsnNamedField {820name: parse_quote!(x),821value: Some(BsnValue::Expr(quote!({}))),822is_prop: false,823},824BsnNamedField {825name: parse_quote!(x),826value: Some(BsnValue::Expr(quote!({}))),827is_prop: false,828},829]),830})),831}]),832};833834let res = nested_duplicate.to_patch_tokens(835&mut ctx,836&mut assignments,837true,838false,839false,840PatchTarget {841path: &[],842is_ref: false,843},844);845846assert!(res.is_ok());847assert_eq!(ctx.errors.len(), 1);848assert!(ctx.errors[0]849.to_string()850.contains("Duplicate field `x` found in BSN struct"));851}852853#[test]854fn missing_struct_field() {855let mut refs = EntityRefs::default();856let paths = TestPaths::new();857let mut exprs = HoistedExpressions::default();858let mut ctx = paths.ctx(&mut refs, &mut exprs);859let mut assignments = Vec::new();860let missing = BsnType {861path: parse_quote!(Transform),862enum_variant: None,863fields: BsnFields::Named(vec![BsnNamedField {864is_prop: false,865name: parse_quote!(x),866value: None,867}]),868};869870let res = missing.push_struct_patch(871&mut ctx,872&mut assignments,873false,874false,875PatchTarget {876path: &[Member::Named(parse_quote!(value))],877is_ref: false,878},879);880881assert!(res.is_ok());882assert_eq!(ctx.errors.len(), 1);883assert!(ctx.errors[0]884.to_string()885.contains("Field `x` is missing a value"));886}887888#[test]889fn enum_duplicate_field() {890// Arrange891let mut refs = EntityRefs::default();892let paths = TestPaths::new();893let mut exprs = HoistedExpressions::default();894let mut ctx = paths.ctx(&mut refs, &mut exprs);895let mut assignments = vec![];896let duplicate = BsnType {897path: parse_quote!(MyEnum),898enum_variant: Some(parse_quote!(Variant)),899fields: BsnFields::Named(vec![900BsnNamedField {901is_prop: false,902name: parse_quote!(x),903value: Some(BsnValue::Expr(quote!(1))),904},905BsnNamedField {906is_prop: false,907name: parse_quote!(x),908value: Some(BsnValue::Expr(quote!(2))),909},910]),911};912913// Act914let res = duplicate.push_enum_patch(915&mut ctx,916&parse_quote!(Variant),917&mut assignments,918PatchTarget {919path: &[],920is_ref: false,921},922);923924// Assert925assert!(res.is_ok());926assert_eq!(ctx.errors.len(), 1);927assert!(ctx.errors[0]928.to_string()929.contains("Duplicate field `x` found in BSN enum variant"));930}931932#[test]933fn bsn_root_preserves_inference_on_error() {934// Arrange935let expected = "bevy_scene :: SceneScope ({ let _res = bevy_scene :: auto_nest_tuple \936! () ; :: core :: compile_error ! { \"Test Error\" } _res })";937938let mut refs = EntityRefs::default();939let paths = TestPaths::new();940let mut exprs = HoistedExpressions::default();941let mut ctx = paths.ctx(&mut refs, &mut exprs);942ctx.errors.push(syn::Error::new(943proc_macro2::Span::call_site(),944"Test Error",945));946let root = BsnRoot(Bsn::<true> { entries: vec![] });947948// Act949let res = root.to_tokens(&mut ctx).to_string();950951// Assert952assert_eq!(res, expected,);953}954955#[test]956fn bsn_list_root_preserves_inference_on_error() {957// Arrange958let expected =959"{ let _res = bevy_scene :: SceneListScope (bevy_scene :: auto_nest_tuple ! ()) ;"960.to_string()961+ " :: core :: compile_error ! { \"Test Error\" }"962+ " _res }";963964let mut refs = EntityRefs::default();965let paths = TestPaths::new();966let mut exprs = HoistedExpressions::default();967let mut ctx = paths.ctx(&mut refs, &mut exprs);968ctx.errors.push(syn::Error::new(969proc_macro2::Span::call_site(),970"Test Error",971));972let root = BsnListRoot(BsnSceneListItems(vec![]));973974// Act975let res = root.to_tokens(&mut ctx).to_string();976977// Assert978assert_eq!(res, expected,);979}980}981982983