Skip to content

Commit

Permalink
bevy_reflect: Fix combined field attributes (#9322)
Browse files Browse the repository at this point in the history
# Objective

It seems the behavior of field attributes was accidentally broken at
some point. Take the following code:

```rust
#[derive(Reflect)]
struct Foo {
  #[reflect(ignore, default)]
  value: usize
}
```

The above code should simply mark `value` as ignored and specify a
default behavior. However, what this actually does is discard both.
That's especially a problem when we don't want the field to be be given
a `Reflect` or `FromReflect` bound (which is why we ignore it in the
first place).

This only happens when the attributes are combined into one. The
following code works properly:

```rust
#[derive(Reflect)]
struct Foo {
  #[reflect(ignore)]
  #[reflect(default)]
  value: usize
}
```

## Solution

Cleaned up the field attribute parsing logic to support combined field
attributes.

---

## Changelog

- Fixed a bug where `Reflect` derive attributes on fields are not able
to be combined into a single attribute
  • Loading branch information
MrGVSV authored and cart committed Aug 10, 2023
1 parent 2a02929 commit be2301d
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 54 deletions.
95 changes: 47 additions & 48 deletions crates/bevy_reflect/bevy_reflect_derive/src/field_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
//! the derive helper attribute for `Reflect`, which looks like: `#[reflect(ignore)]`.

use crate::REFLECT_ATTRIBUTE_NAME;
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{Attribute, Expr, ExprLit, Lit, Meta};
use syn::meta::ParseNestedMeta;
use syn::{Attribute, LitStr, Token};

pub(crate) static IGNORE_SERIALIZATION_ATTR: &str = "skip_serializing";
pub(crate) static IGNORE_ALL_ATTR: &str = "ignore";
Expand Down Expand Up @@ -78,7 +77,8 @@ pub(crate) fn parse_field_attrs(attrs: &[Attribute]) -> Result<ReflectFieldAttr,
.iter()
.filter(|a| a.path().is_ident(REFLECT_ATTRIBUTE_NAME));
for attr in attrs {
if let Err(err) = parse_meta(&mut args, &attr.meta) {
let result = attr.parse_nested_meta(|meta| parse_meta(&mut args, meta));
if let Err(err) = result {
if let Some(ref mut error) = errors {
error.combine(err);
} else {
Expand All @@ -94,54 +94,53 @@ pub(crate) fn parse_field_attrs(attrs: &[Attribute]) -> Result<ReflectFieldAttr,
}
}

/// Recursively parses attribute metadata for things like `#[reflect(ignore)]` and `#[reflect(default = "foo")]`
fn parse_meta(args: &mut ReflectFieldAttr, meta: &Meta) -> Result<(), syn::Error> {
match meta {
Meta::Path(path) if path.is_ident(IGNORE_SERIALIZATION_ATTR) => {
(args.ignore == ReflectIgnoreBehavior::None)
.then(|| args.ignore = ReflectIgnoreBehavior::IgnoreSerialization)
.ok_or_else(|| syn::Error::new_spanned(path, format!("Only one of ['{IGNORE_SERIALIZATION_ATTR}','{IGNORE_ALL_ATTR}'] is allowed")))
fn parse_meta(args: &mut ReflectFieldAttr, meta: ParseNestedMeta) -> Result<(), syn::Error> {
if meta.path.is_ident(DEFAULT_ATTR) {
// Allow:
// - `#[reflect(default)]`
// - `#[reflect(default = "path::to::func")]`
if !matches!(args.default, DefaultBehavior::Required) {
return Err(meta.error(format!("only one of [{:?}] is allowed", [DEFAULT_ATTR])));
}
Meta::Path(path) if path.is_ident(IGNORE_ALL_ATTR) => {
(args.ignore == ReflectIgnoreBehavior::None)
.then(|| args.ignore = ReflectIgnoreBehavior::IgnoreAlways)
.ok_or_else(|| syn::Error::new_spanned(path, format!("Only one of ['{IGNORE_SERIALIZATION_ATTR}','{IGNORE_ALL_ATTR}'] is allowed")))
}
Meta::Path(path) if path.is_ident(DEFAULT_ATTR) => {

if meta.input.peek(Token![=]) {
let lit = meta.value()?.parse::<LitStr>()?;
args.default = DefaultBehavior::Func(lit.parse()?);
} else {
args.default = DefaultBehavior::Default;
Ok(())
}
Meta::Path(path) => Err(syn::Error::new(
path.span(),
format!("unknown attribute parameter: {}", path.to_token_stream()),
)),
Meta::NameValue(pair) if pair.path.is_ident(DEFAULT_ATTR) => {
if let Expr::Lit(ExprLit {lit: Lit::Str(lit_str), ..}) = &pair.value {
args.default = DefaultBehavior::Func(lit_str.parse()?);
Ok(())
}
else {
Err(syn::Error::new(
pair.span(),
format!("expected a string literal containing the name of a function, but found: {}", pair.to_token_stream()),
))?
}
}
Meta::NameValue(pair) => {
let path = &pair.path;
Err(syn::Error::new(
path.span(),
format!("unknown attribute parameter: {}", path.to_token_stream()),
))
}
Meta::List(list) if !list.path.is_ident(REFLECT_ATTRIBUTE_NAME) => {
Err(syn::Error::new(list.path.span(), "unexpected property"))

Ok(())
} else if meta.path.is_ident(IGNORE_ALL_ATTR) {
// Allow:
// - `#[reflect(ignore)]`
if args.ignore != ReflectIgnoreBehavior::None {
return Err(meta.error(format!(
"only one of [{:?}] is allowed",
[IGNORE_ALL_ATTR, IGNORE_SERIALIZATION_ATTR]
)));
}
Meta::List(list) => {
if let Ok(meta) = list.parse_args() {
parse_meta(args, &meta)?;
}
Ok(())

args.ignore = ReflectIgnoreBehavior::IgnoreAlways;

Ok(())
} else if meta.path.is_ident(IGNORE_SERIALIZATION_ATTR) {
// Allow:
// - `#[reflect(skip_serializing)]`
if args.ignore != ReflectIgnoreBehavior::None {
return Err(meta.error(format!(
"only one of [{:?}] is allowed",
[IGNORE_ALL_ATTR, IGNORE_SERIALIZATION_ATTR]
)));
}

args.ignore = ReflectIgnoreBehavior::IgnoreSerialization;

Ok(())
} else {
Err(meta.error(format!(
"unknown attribute, expected {:?}",
[DEFAULT_ATTR, IGNORE_ALL_ATTR, IGNORE_SERIALIZATION_ATTR]
)))
}
}
2 changes: 1 addition & 1 deletion crates/bevy_reflect/bevy_reflect_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ pub fn derive_reflect(input: TokenStream) -> TokenStream {
///
/// By default, this attribute denotes that the field's type implements [`Default`].
/// However, it can also take in a path string to a user-defined function that will return the default value.
/// This takes the form: `#[reflect(default = "path::to::my_function)]` where `my_function` is a parameterless
/// This takes the form: `#[reflect(default = "path::to::my_function")]` where `my_function` is a parameterless
/// function that must return some default value for the type.
///
/// Specifying a custom default can be used to give different fields their own specialized defaults,
Expand Down
18 changes: 13 additions & 5 deletions crates/bevy_reflect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,18 +749,26 @@ mod tests {
foo: String,

// Use `get_bar_default()`
#[reflect(default = "get_bar_default")]
#[reflect(ignore)]
bar: usize,
#[reflect(default = "get_bar_default")]
bar: NotReflect,

// Ensure attributes can be combined
#[reflect(ignore, default = "get_bar_default")]
baz: NotReflect,
}

fn get_bar_default() -> usize {
123
#[derive(Eq, PartialEq, Debug)]
struct NotReflect(usize);

fn get_bar_default() -> NotReflect {
NotReflect(123)
}

let expected = MyStruct {
foo: String::default(),
bar: 123,
bar: NotReflect(123),
baz: NotReflect(123),
};

let dyn_struct = DynamicStruct::default();
Expand Down

0 comments on commit be2301d

Please sign in to comment.