diff --git a/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr index 0315f2be74..46fec78c43 100644 --- a/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr +++ b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr @@ -1,14 +1,11 @@ error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied - --> tests/deref_mut_derive/missing_deref_fail.rs:10:8 - | -10 | struct TupleStruct(usize, #[deref] String); - | ^^^^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct` - | + --> tests/deref_mut_derive/missing_deref_fail.rs:9:8 + | +9 | struct TupleStruct(usize, #[deref] String); + | ^^^^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct` + | note: required by a bound in `DerefMut` - --> $RUSTUP_HOME/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust/library/core/src/ops/deref.rs:264:21 - | -264 | pub trait DerefMut: Deref { - | ^^^^^ required by this bound in `DerefMut` + --> /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/ops/deref.rs:290:1 error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied --> tests/deref_mut_derive/missing_deref_fail.rs:7:10 @@ -19,21 +16,18 @@ error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied = note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Struct: Deref` is not satisfied - --> tests/deref_mut_derive/missing_deref_fail.rs:15:8 - | -15 | struct Struct { - | ^^^^^^ the trait `Deref` is not implemented for `Struct` - | + --> tests/deref_mut_derive/missing_deref_fail.rs:14:8 + | +14 | struct Struct { + | ^^^^^^ the trait `Deref` is not implemented for `Struct` + | note: required by a bound in `DerefMut` - --> $RUSTUP_HOME/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust/library/core/src/ops/deref.rs:264:21 - | -264 | pub trait DerefMut: Deref { - | ^^^^^ required by this bound in `DerefMut` + --> /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/ops/deref.rs:290:1 error[E0277]: the trait bound `Struct: Deref` is not satisfied - --> tests/deref_mut_derive/missing_deref_fail.rs:13:10 + --> tests/deref_mut_derive/missing_deref_fail.rs:12:10 | -13 | #[derive(DerefMut)] +12 | #[derive(DerefMut)] | ^^^^^^^^ the trait `Deref` is not implemented for `Struct` | = note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.rs new file mode 100644 index 0000000000..ef6b98cf09 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.rs @@ -0,0 +1,14 @@ +use bevy_ecs::prelude::*; + +// this should fail since the function is required to have the signature +// (DeferredWorld, HookContext) -> () +#[derive(Component)] +//~^ E0057 +#[component( + on_add = wrong_bazzing("foo"), +)] +pub struct FooWrongCall; + +fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) { + |world| {} +} diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.stderr b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.stderr new file mode 100644 index 0000000000..967cffe4ff --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_call_signature_mismatch.stderr @@ -0,0 +1,32 @@ +warning: unused variable: `path` + --> tests/ui/component_hook_call_signature_mismatch.rs:12:18 + | +12 | fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) { + | ^^^^ help: if this is intentional, prefix it with an underscore: `_path` + | + = note: `#[warn(unused_variables)]` on by default + +warning: unused variable: `world` + --> tests/ui/component_hook_call_signature_mismatch.rs:13:6 + | +13 | |world| {} + | ^^^^^ help: if this is intentional, prefix it with an underscore: `_world` + +error[E0057]: this function takes 1 argument but 2 arguments were supplied + --> tests/ui/component_hook_call_signature_mismatch.rs:8:14 + | +5 | #[derive(Component)] + | --------- unexpected argument #2 of type `HookContext` +... +8 | on_add = wrong_bazzing("foo"), + | ^^^^^^^^^^^^^^^^^^^^ + | +note: opaque type defined here + --> tests/ui/component_hook_call_signature_mismatch.rs:12:33 + | +12 | fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 1 previous error; 2 warnings emitted + +For more information about this error, try `rustc --explain E0057`. diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs new file mode 100644 index 0000000000..4076819ee3 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs @@ -0,0 +1,63 @@ +use bevy_ecs::prelude::*; + +mod case1 { + use super::*; + + #[derive(Component, Debug)] + #[component(on_insert = foo_hook)] + //~^ ERROR: Custom on_insert hooks are not supported as relationships already define an on_insert hook + #[relationship(relationship_target = FooTargets)] + pub struct FooTargetOfFail(Entity); + + #[derive(Component, Debug)] + #[relationship_target(relationship = FooTargetOfFail)] + //~^ E0277 + pub struct FooTargets(Vec); +} + +mod case2 { + use super::*; + + #[derive(Component, Debug)] + #[component(on_replace = foo_hook)] + //~^ ERROR: Custom on_replace hooks are not supported as RelationshipTarget already defines an on_replace hook + #[relationship_target(relationship = FooTargetOf)] + pub struct FooTargetsFail(Vec); + + #[derive(Component, Debug)] + #[relationship(relationship_target = FooTargetsFail)] + //~^ E0277 + pub struct FooTargetOf(Entity); +} + +mod case3 { + use super::*; + + #[derive(Component, Debug)] + #[component(on_replace = foo_hook)] + //~^ ERROR: Custom on_replace hooks are not supported as Relationships already define an on_replace hook + #[relationship(relationship_target = BarTargets)] + pub struct BarTargetOfFail(Entity); + + #[derive(Component, Debug)] + #[relationship_target(relationship = BarTargetOfFail)] + //~^ E0277 + pub struct BarTargets(Vec); +} + +mod case4 { + use super::*; + + #[derive(Component, Debug)] + #[component(on_despawn = foo_hook)] + //~^ ERROR: Custom on_despawn hooks are not supported as this RelationshipTarget already defines an on_despawn hook, via the 'linked_spawn' attribute + #[relationship_target(relationship = BarTargetOf, linked_spawn)] + pub struct BarTargetsFail(Vec); + + #[derive(Component, Debug)] + #[relationship(relationship_target = BarTargetsFail)] + //~^ E0277 + pub struct BarTargetOf(Entity); +} + +fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.stderr b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.stderr new file mode 100644 index 0000000000..01e4d57578 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.stderr @@ -0,0 +1,91 @@ +error: Custom on_insert hooks are not supported as relationships already define an on_insert hook + --> tests/ui/component_hook_relationship.rs:7:5 + | +7 | #[component(on_insert = foo_hook)] + | ^ + +error: Custom on_replace hooks are not supported as RelationshipTarget already defines an on_replace hook + --> tests/ui/component_hook_relationship.rs:22:5 + | +22 | #[component(on_replace = foo_hook)] + | ^ + +error: Custom on_replace hooks are not supported as Relationships already define an on_replace hook + --> tests/ui/component_hook_relationship.rs:37:5 + | +37 | #[component(on_replace = foo_hook)] + | ^ + +error: Custom on_despawn hooks are not supported as this RelationshipTarget already defines an on_despawn hook, via the 'linked_spawn' attribute + --> tests/ui/component_hook_relationship.rs:52:5 + | +52 | #[component(on_despawn = foo_hook)] + | ^ + +error[E0277]: the trait bound `FooTargetOfFail: Relationship` is not satisfied + --> tests/ui/component_hook_relationship.rs:13:42 + | +13 | #[relationship_target(relationship = FooTargetOfFail)] + | ^^^^^^^^^^^^^^^ the trait `Relationship` is not implemented for `FooTargetOfFail` + | + = help: the following other types implement trait `Relationship`: + BarTargetOf + ChildOf + FooTargetOf +note: required by a bound in `bevy_ecs::relationship::RelationshipTarget::Relationship` + --> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:167:24 + | +167 | type Relationship: Relationship; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RelationshipTarget::Relationship` + +error[E0277]: the trait bound `FooTargetsFail: bevy_ecs::relationship::RelationshipTarget` is not satisfied + --> tests/ui/component_hook_relationship.rs:28:42 + | +28 | #[relationship(relationship_target = FooTargetsFail)] + | ^^^^^^^^^^^^^^ the trait `bevy_ecs::relationship::RelationshipTarget` is not implemented for `FooTargetsFail` + | + = help: the following other types implement trait `bevy_ecs::relationship::RelationshipTarget`: + BarTargets + Children + FooTargets +note: required by a bound in `bevy_ecs::relationship::Relationship::RelationshipTarget` + --> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:79:30 + | +79 | type RelationshipTarget: RelationshipTarget; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Relationship::RelationshipTarget` + +error[E0277]: the trait bound `BarTargetOfFail: Relationship` is not satisfied + --> tests/ui/component_hook_relationship.rs:43:42 + | +43 | #[relationship_target(relationship = BarTargetOfFail)] + | ^^^^^^^^^^^^^^^ the trait `Relationship` is not implemented for `BarTargetOfFail` + | + = help: the following other types implement trait `Relationship`: + BarTargetOf + ChildOf + FooTargetOf +note: required by a bound in `bevy_ecs::relationship::RelationshipTarget::Relationship` + --> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:167:24 + | +167 | type Relationship: Relationship; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RelationshipTarget::Relationship` + +error[E0277]: the trait bound `BarTargetsFail: bevy_ecs::relationship::RelationshipTarget` is not satisfied + --> tests/ui/component_hook_relationship.rs:58:42 + | +58 | #[relationship(relationship_target = BarTargetsFail)] + | ^^^^^^^^^^^^^^ the trait `bevy_ecs::relationship::RelationshipTarget` is not implemented for `BarTargetsFail` + | + = help: the following other types implement trait `bevy_ecs::relationship::RelationshipTarget`: + BarTargets + Children + FooTargets +note: required by a bound in `bevy_ecs::relationship::Relationship::RelationshipTarget` + --> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:79:30 + | +79 | type RelationshipTarget: RelationshipTarget; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Relationship::RelationshipTarget` + +error: aborting due to 8 previous errors + +For more information about this error, try `rustc --explain E0277`. diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.rs new file mode 100644 index 0000000000..7670a26106 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.rs @@ -0,0 +1,14 @@ +use bevy_ecs::prelude::*; + +// the proc macro allows general paths, which means normal structs are also passing the basic +// parsing. This test makes sure that we don't accidentally allow structs as hooks through future +// changes. +// +// Currently the error is thrown in the generated code and not while executing the proc macro +// logic. +#[derive(Component)] +#[component( + on_add = Bar, + //~^ E0425 +)] +pub struct FooWrongPath; diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.stderr b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.stderr new file mode 100644 index 0000000000..4415582709 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_struct_path.stderr @@ -0,0 +1,9 @@ +error[E0425]: cannot find value `Bar` in this scope + --> tests/ui/component_hook_struct_path.rs:11:14 + | +11 | on_add = Bar, + | ^^^ not found in this scope + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0425`. diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index f88ae4349c..48a7715b85 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -9,8 +9,8 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::{Comma, Paren}, - Data, DataStruct, DeriveInput, ExprClosure, ExprPath, Field, Fields, Ident, Index, LitStr, - Member, Path, Result, Token, Type, Visibility, + Data, DataStruct, DeriveInput, Expr, ExprCall, ExprClosure, ExprPath, Field, Fields, Ident, + Index, LitStr, Member, Path, Result, Token, Type, Visibility, }; pub fn derive_event(input: TokenStream) -> TokenStream { @@ -80,8 +80,12 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let storage = storage_path(&bevy_ecs_path, attrs.storage); - let on_add_path = attrs.on_add.map(|path| path.to_token_stream()); - let on_remove_path = attrs.on_remove.map(|path| path.to_token_stream()); + let on_add_path = attrs + .on_add + .map(|path| path.to_token_stream(&bevy_ecs_path)); + let on_remove_path = attrs + .on_remove + .map(|path| path.to_token_stream(&bevy_ecs_path)); let on_insert_path = if relationship.is_some() { if attrs.on_insert.is_some() { @@ -95,7 +99,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream { Some(quote!(::on_insert)) } else { - attrs.on_insert.map(|path| path.to_token_stream()) + attrs + .on_insert + .map(|path| path.to_token_stream(&bevy_ecs_path)) }; let on_replace_path = if relationship.is_some() { @@ -121,7 +127,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream { Some(quote!(::on_replace)) } else { - attrs.on_replace.map(|path| path.to_token_stream()) + attrs + .on_replace + .map(|path| path.to_token_stream(&bevy_ecs_path)) }; let on_despawn_path = if attrs @@ -139,7 +147,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream { Some(quote!(::on_despawn)) } else { - attrs.on_despawn.map(|path| path.to_token_stream()) + attrs + .on_despawn + .map(|path| path.to_token_stream(&bevy_ecs_path)) }; let on_add = hook_register_function_call(&bevy_ecs_path, quote! {on_add}, on_add_path); @@ -441,14 +451,64 @@ pub const ON_DESPAWN: &str = "on_despawn"; pub const IMMUTABLE: &str = "immutable"; +/// All allowed attribute value expression kinds for component hooks +#[derive(Debug)] +enum HookAttributeKind { + /// expressions like function or struct names + /// + /// structs will throw compile errors on the code generation so this is safe + Path(ExprPath), + /// function call like expressions + Call(ExprCall), +} + +impl HookAttributeKind { + fn from_expr(value: Expr) -> Result { + match value { + Expr::Path(path) => Ok(HookAttributeKind::Path(path)), + Expr::Call(call) => Ok(HookAttributeKind::Call(call)), + // throw meaningful error on all other expressions + _ => Err(syn::Error::new( + value.span(), + [ + "Not supported in this position, please use one of the following:", + "- path to function", + "- call to function yielding closure", + ] + .join("\n"), + )), + } + } + + fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 { + match self { + HookAttributeKind::Path(path) => path.to_token_stream(), + HookAttributeKind::Call(call) => { + quote!({ + fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { + (#call)(world, ctx) + } + _internal_hook + }) + } + } + } +} + +impl Parse for HookAttributeKind { + fn parse(input: syn::parse::ParseStream) -> Result { + input.parse::().and_then(Self::from_expr) + } +} + struct Attrs { storage: StorageTy, requires: Option>, - on_add: Option, - on_insert: Option, - on_replace: Option, - on_remove: Option, - on_despawn: Option, + on_add: Option, + on_insert: Option, + on_replace: Option, + on_remove: Option, + on_despawn: Option, relationship: Option, relationship_target: Option, immutable: bool, @@ -513,19 +573,19 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { }; Ok(()) } else if nested.path.is_ident(ON_ADD) { - attrs.on_add = Some(nested.value()?.parse::()?); + attrs.on_add = Some(nested.value()?.parse::()?); Ok(()) } else if nested.path.is_ident(ON_INSERT) { - attrs.on_insert = Some(nested.value()?.parse::()?); + attrs.on_insert = Some(nested.value()?.parse::()?); Ok(()) } else if nested.path.is_ident(ON_REPLACE) { - attrs.on_replace = Some(nested.value()?.parse::()?); + attrs.on_replace = Some(nested.value()?.parse::()?); Ok(()) } else if nested.path.is_ident(ON_REMOVE) { - attrs.on_remove = Some(nested.value()?.parse::()?); + attrs.on_remove = Some(nested.value()?.parse::()?); Ok(()) } else if nested.path.is_ident(ON_DESPAWN) { - attrs.on_despawn = Some(nested.value()?.parse::()?); + attrs.on_despawn = Some(nested.value()?.parse::()?); Ok(()) } else if nested.path.is_ident(IMMUTABLE) { attrs.immutable = true; diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 7a67571d89..fb72de9cec 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -319,6 +319,25 @@ use thiserror::Error; /// } /// ``` /// +/// This also supports function calls that yield closures +/// +/// ``` +/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::world::DeferredWorld; +/// # +/// #[derive(Component)] +/// #[component(on_add = my_msg_hook("hello"))] +/// #[component(on_despawn = my_msg_hook("yoink"))] +/// struct ComponentA; +/// +/// // a hook closure generating function +/// fn my_msg_hook(message: &'static str) -> impl Fn(DeferredWorld, HookContext) { +/// move |_world, _ctx| { +/// println!("{message}"); +/// } +/// } +/// ``` +/// /// # Implementing the trait for foreign types /// /// As a consequence of the [orphan rule], it is not possible to separate into two different crates the implementation of `Component` from the definition of a type.