Skip to content

Commit 132e8fb

Browse files
committed
Support multiple #[reflect]/#[reflect_value] + improve error messages (#6237)
# Objective Currently, surprising behavior happens when specifying `#[reflect(...)]` or `#[reflect_value(...)]` multiple times. Rather than merging the traits lists from all attributes, only the trait list from the last attribute is used. For example, in the following code, only the `Debug` and `Hash` traits are reflected and not `Default` or `PartialEq`: ```rs #[derive(Debug, PartialEq, Hash, Default, Reflect)] #[reflect(PartialEq, Default)] #[reflect(Debug, Hash)] struct Foo; ``` This is especially important when some traits should only be reflected under certain circumstances. For example, this previously had surprisingly behavior when the "serialize" feature is enabled: ```rs #[derive(Debug, Hash, Reflect)] #[reflect(Debug, Hash)] #[cfg_attr( feature = "serialize", derive(Serialize, Deserialize), reflect(Serialize, Deserialize) ] struct Foo; ``` In addition, compile error messages generated from using the derive macro often point to the `#[derive(Reflect)]` rather than to the source of the error. It would be a lot more helpful if the compiler errors pointed to what specifically caused the error rather than just to the derive macro itself. ## Solution Merge the trait lists in all `#[reflect(...)]` and `#[reflect_value(...)]` attributes. Additionally, make `#[reflect]` and `#[reflect_value]` mutually exclusive. Additionally, span information is carried throughout some parts of the code now to ensure that error messages point to more useful places and better indicate what caused those errors. For example, `#[reflect(Hash, Hash)]` points to the second `Hash` as the source of an error. Also, in the following example, the compiler error now points to the `Hash` in `#[reflect(Hash)]` rather than to the derive macro: ```rs #[derive(Reflect)] #[reflect(Hash)] // <-- compiler error points to `Hash` for lack of a `Hash` implementation struct Foo; ``` --- ## Changelog Changed - Using multiple `#[reflect(...)]` or `#[reflect_value(...)]` attributes now merges the trait lists. For example, `#[reflect(Debug, Hash)] #[reflect(PartialEq, Default)]` is equivalent to `#[reflect(Debug, Hash, PartialEq, Default)]`. - Multiple `#[reflect(...)]` and `#[reflect_value(...)]` attributes were previously accepted, but only the last attribute was respected. - Using both `#[reflect(...)]` and `#[reflect_value(...)]` was previously accepted, but had surprising behavior. This is no longer accepted. - Improved error messages for `#[derive(Reflect)]` by propagating useful span information. Many errors should now point to the source of those errors rather than to the derive macro.
1 parent c11fbfb commit 132e8fb

File tree

3 files changed

+192
-34
lines changed

3 files changed

+192
-34
lines changed

crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
//! `#[reflect(PartialEq, Default, ...)]` and `#[reflect_value(PartialEq, Default, ...)]`.
77
88
use crate::utility;
9-
use proc_macro2::Ident;
10-
use quote::quote;
9+
use proc_macro2::{Ident, Span};
10+
use quote::quote_spanned;
1111
use syn::parse::{Parse, ParseStream};
1212
use syn::punctuated::Punctuated;
13+
use syn::spanned::Spanned;
1314
use syn::token::Comma;
1415
use syn::{Meta, NestedMeta, Path};
1516

@@ -23,19 +24,39 @@ const HASH_ATTR: &str = "Hash";
2324
// but useful to know exist nonetheless
2425
pub(crate) const REFLECT_DEFAULT: &str = "ReflectDefault";
2526

27+
// The error message to show when a trait/type is specified multiple times
28+
const CONFLICTING_TYPE_DATA_MESSAGE: &str = "conflicting type data registration";
29+
2630
/// A marker for trait implementations registered via the `Reflect` derive macro.
2731
#[derive(Clone, Default)]
2832
pub(crate) enum TraitImpl {
2933
/// The trait is not registered as implemented.
3034
#[default]
3135
NotImplemented,
36+
3237
/// The trait is registered as implemented.
33-
Implemented,
38+
Implemented(Span),
3439

35-
// TODO: This can be made to use `ExprPath` instead of `Ident`, allowing for fully qualified paths to be used
3640
/// The trait is registered with a custom function rather than an actual implementation.
37-
Custom(Ident),
41+
Custom(Path, Span),
42+
}
43+
44+
impl TraitImpl {
45+
/// Merges this [`TraitImpl`] with another.
46+
///
47+
/// Returns whichever value is not [`TraitImpl::NotImplemented`].
48+
/// If both values are [`TraitImpl::NotImplemented`], then that is returned.
49+
/// Otherwise, an error is returned if neither value is [`TraitImpl::NotImplemented`].
50+
pub fn merge(self, other: TraitImpl) -> Result<TraitImpl, syn::Error> {
51+
match (self, other) {
52+
(TraitImpl::NotImplemented, value) | (value, TraitImpl::NotImplemented) => Ok(value),
53+
(_, TraitImpl::Implemented(span) | TraitImpl::Custom(_, span)) => {
54+
Err(syn::Error::new(span, CONFLICTING_TYPE_DATA_MESSAGE))
55+
}
56+
}
57+
}
3858
}
59+
3960
/// A collection of traits that have been registered for a reflected type.
4061
///
4162
/// This keeps track of a few traits that are utilized internally for reflection
@@ -107,25 +128,45 @@ pub(crate) struct ReflectTraits {
107128

108129
impl ReflectTraits {
109130
/// Create a new [`ReflectTraits`] instance from a set of nested metas.
110-
pub fn from_nested_metas(nested_metas: &Punctuated<NestedMeta, Comma>) -> Self {
131+
pub fn from_nested_metas(
132+
nested_metas: &Punctuated<NestedMeta, Comma>,
133+
) -> Result<Self, syn::Error> {
111134
let mut traits = ReflectTraits::default();
112135
for nested_meta in nested_metas.iter() {
113136
match nested_meta {
114137
// Handles `#[reflect( Hash, Default, ... )]`
115138
NestedMeta::Meta(Meta::Path(path)) => {
116139
// Get the first ident in the path (hopefully the path only contains one and not `std::hash::Hash`)
117140
let ident = if let Some(segment) = path.segments.iter().next() {
118-
segment.ident.to_string()
141+
&segment.ident
119142
} else {
120143
continue;
121144
};
145+
let ident_name = ident.to_string();
146+
147+
// Track the span where the trait is implemented for future errors
148+
let span = ident.span();
122149

123-
match ident.as_str() {
124-
DEBUG_ATTR => traits.debug = TraitImpl::Implemented,
125-
PARTIAL_EQ_ATTR => traits.partial_eq = TraitImpl::Implemented,
126-
HASH_ATTR => traits.hash = TraitImpl::Implemented,
150+
match ident_name.as_str() {
151+
DEBUG_ATTR => {
152+
traits.debug = traits.debug.merge(TraitImpl::Implemented(span))?;
153+
}
154+
PARTIAL_EQ_ATTR => {
155+
traits.partial_eq =
156+
traits.partial_eq.merge(TraitImpl::Implemented(span))?;
157+
}
158+
HASH_ATTR => {
159+
traits.hash = traits.hash.merge(TraitImpl::Implemented(span))?;
160+
}
127161
// We only track reflected idents for traits not considered special
128-
_ => traits.idents.push(utility::get_reflect_ident(&ident)),
162+
_ => {
163+
// Create the reflect ident
164+
// We set the span to the old ident so any compile errors point to that ident instead
165+
let mut reflect_ident = utility::get_reflect_ident(&ident_name);
166+
reflect_ident.set_span(span);
167+
168+
add_unique_ident(&mut traits.idents, reflect_ident)?;
169+
}
129170
}
130171
}
131172
// Handles `#[reflect( Hash(custom_hash_fn) )]`
@@ -137,25 +178,32 @@ impl ReflectTraits {
137178
continue;
138179
};
139180

181+
// Track the span where the trait is implemented for future errors
182+
let span = ident.span();
183+
140184
let list_meta = list.nested.iter().next();
141185
if let Some(NestedMeta::Meta(Meta::Path(path))) = list_meta {
142-
if let Some(segment) = path.segments.iter().next() {
143-
// This should be the ident of the custom function
144-
let trait_func_ident = TraitImpl::Custom(segment.ident.clone());
145-
match ident.as_str() {
146-
DEBUG_ATTR => traits.debug = trait_func_ident,
147-
PARTIAL_EQ_ATTR => traits.partial_eq = trait_func_ident,
148-
HASH_ATTR => traits.hash = trait_func_ident,
149-
_ => {}
186+
// This should be the path of the custom function
187+
let trait_func_ident = TraitImpl::Custom(path.clone(), span);
188+
match ident.as_str() {
189+
DEBUG_ATTR => {
190+
traits.debug = traits.debug.merge(trait_func_ident)?;
191+
}
192+
PARTIAL_EQ_ATTR => {
193+
traits.partial_eq = traits.partial_eq.merge(trait_func_ident)?;
150194
}
195+
HASH_ATTR => {
196+
traits.hash = traits.hash.merge(trait_func_ident)?;
197+
}
198+
_ => {}
151199
}
152200
}
153201
}
154202
_ => {}
155203
}
156204
}
157205

158-
traits
206+
Ok(traits)
159207
}
160208

161209
/// Returns true if the given reflected trait name (i.e. `ReflectDefault` for `Default`)
@@ -174,7 +222,7 @@ impl ReflectTraits {
174222
/// If `Hash` was not registered, returns `None`.
175223
pub fn get_hash_impl(&self, bevy_reflect_path: &Path) -> Option<proc_macro2::TokenStream> {
176224
match &self.hash {
177-
TraitImpl::Implemented => Some(quote! {
225+
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
178226
fn reflect_hash(&self) -> Option<u64> {
179227
use std::hash::{Hash, Hasher};
180228
let mut hasher = #bevy_reflect_path::ReflectHasher::default();
@@ -183,7 +231,7 @@ impl ReflectTraits {
183231
Some(hasher.finish())
184232
}
185233
}),
186-
TraitImpl::Custom(impl_fn) => Some(quote! {
234+
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
187235
fn reflect_hash(&self) -> Option<u64> {
188236
Some(#impl_fn(self))
189237
}
@@ -200,7 +248,7 @@ impl ReflectTraits {
200248
bevy_reflect_path: &Path,
201249
) -> Option<proc_macro2::TokenStream> {
202250
match &self.partial_eq {
203-
TraitImpl::Implemented => Some(quote! {
251+
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
204252
fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> Option<bool> {
205253
let value = value.as_any();
206254
if let Some(value) = value.downcast_ref::<Self>() {
@@ -210,7 +258,7 @@ impl ReflectTraits {
210258
}
211259
}
212260
}),
213-
TraitImpl::Custom(impl_fn) => Some(quote! {
261+
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
214262
fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> Option<bool> {
215263
Some(#impl_fn(self, value))
216264
}
@@ -224,24 +272,55 @@ impl ReflectTraits {
224272
/// If `Debug` was not registered, returns `None`.
225273
pub fn get_debug_impl(&self) -> Option<proc_macro2::TokenStream> {
226274
match &self.debug {
227-
TraitImpl::Implemented => Some(quote! {
275+
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
228276
fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229277
std::fmt::Debug::fmt(self, f)
230278
}
231279
}),
232-
TraitImpl::Custom(impl_fn) => Some(quote! {
280+
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
233281
fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234282
#impl_fn(self, f)
235283
}
236284
}),
237285
TraitImpl::NotImplemented => None,
238286
}
239287
}
288+
289+
/// Merges the trait implementations of this [`ReflectTraits`] with another one.
290+
///
291+
/// An error is returned if the two [`ReflectTraits`] have conflicting implementations.
292+
pub fn merge(self, other: ReflectTraits) -> Result<Self, syn::Error> {
293+
Ok(ReflectTraits {
294+
debug: self.debug.merge(other.debug)?,
295+
hash: self.hash.merge(other.hash)?,
296+
partial_eq: self.partial_eq.merge(other.partial_eq)?,
297+
idents: {
298+
let mut idents = self.idents;
299+
for ident in other.idents {
300+
add_unique_ident(&mut idents, ident)?;
301+
}
302+
idents
303+
},
304+
})
305+
}
240306
}
241307

242308
impl Parse for ReflectTraits {
243309
fn parse(input: ParseStream) -> syn::Result<Self> {
244310
let result = Punctuated::<NestedMeta, Comma>::parse_terminated(input)?;
245-
Ok(ReflectTraits::from_nested_metas(&result))
311+
ReflectTraits::from_nested_metas(&result)
246312
}
247313
}
314+
315+
/// Adds an identifier to a vector of identifiers if it is not already present.
316+
///
317+
/// Returns an error if the identifier already exists in the list.
318+
fn add_unique_ident(idents: &mut Vec<Ident>, ident: Ident) -> Result<(), syn::Error> {
319+
let ident_name = ident.to_string();
320+
if idents.iter().any(|i| i == ident_name.as_str()) {
321+
return Err(syn::Error::new(ident.span(), CONFLICTING_TYPE_DATA_MESSAGE));
322+
}
323+
324+
idents.push(ident);
325+
Ok(())
326+
}

crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,65 @@ pub(crate) enum EnumVariantFields<'a> {
108108
Unit,
109109
}
110110

111+
/// The method in which the type should be reflected.
112+
#[derive(PartialEq, Eq)]
113+
enum ReflectMode {
114+
/// Reflect the type normally, providing information about all fields/variants.
115+
Normal,
116+
/// Reflect the type as a value.
117+
Value,
118+
}
119+
111120
impl<'a> ReflectDerive<'a> {
112121
pub fn from_input(input: &'a DeriveInput) -> Result<Self, syn::Error> {
113122
let mut traits = ReflectTraits::default();
114123
// Should indicate whether `#[reflect_value]` was used
115-
let mut force_reflect_value = false;
124+
let mut reflect_mode = None;
116125

117126
for attribute in input.attrs.iter().filter_map(|attr| attr.parse_meta().ok()) {
118127
match attribute {
119128
Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_ATTRIBUTE_NAME) => {
120-
traits = ReflectTraits::from_nested_metas(&meta_list.nested);
129+
if !matches!(reflect_mode, None | Some(ReflectMode::Normal)) {
130+
return Err(syn::Error::new(
131+
meta_list.span(),
132+
format_args!("cannot use both `#[{REFLECT_ATTRIBUTE_NAME}]` and `#[{REFLECT_VALUE_ATTRIBUTE_NAME}]`"),
133+
));
134+
}
135+
136+
reflect_mode = Some(ReflectMode::Normal);
137+
let new_traits = ReflectTraits::from_nested_metas(&meta_list.nested)?;
138+
traits = traits.merge(new_traits)?;
121139
}
122140
Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => {
123-
force_reflect_value = true;
124-
traits = ReflectTraits::from_nested_metas(&meta_list.nested);
141+
if !matches!(reflect_mode, None | Some(ReflectMode::Value)) {
142+
return Err(syn::Error::new(
143+
meta_list.span(),
144+
format_args!("cannot use both `#[{REFLECT_ATTRIBUTE_NAME}]` and `#[{REFLECT_VALUE_ATTRIBUTE_NAME}]`"),
145+
));
146+
}
147+
148+
reflect_mode = Some(ReflectMode::Value);
149+
let new_traits = ReflectTraits::from_nested_metas(&meta_list.nested)?;
150+
traits = traits.merge(new_traits)?;
125151
}
126152
Meta::Path(path) if path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => {
127-
force_reflect_value = true;
153+
if !matches!(reflect_mode, None | Some(ReflectMode::Value)) {
154+
return Err(syn::Error::new(
155+
path.span(),
156+
format_args!("cannot use both `#[{REFLECT_ATTRIBUTE_NAME}]` and `#[{REFLECT_VALUE_ATTRIBUTE_NAME}]`"),
157+
));
158+
}
159+
160+
reflect_mode = Some(ReflectMode::Value);
128161
}
129162
_ => continue,
130163
}
131164
}
132-
if force_reflect_value {
165+
166+
// Use normal reflection if unspecified
167+
let reflect_mode = reflect_mode.unwrap_or(ReflectMode::Normal);
168+
169+
if reflect_mode == ReflectMode::Value {
133170
return Ok(Self::Value(ReflectMeta::new(
134171
&input.ident,
135172
&input.generics,

crates/bevy_reflect/src/lib.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,48 @@ bevy_reflect::tests::should_reflect_debug::Test {
948948
assert_eq!(expected, format!("\n{:#?}", reflected));
949949
}
950950

951+
#[test]
952+
fn multiple_reflect_lists() {
953+
#[derive(Hash, PartialEq, Reflect)]
954+
#[reflect(Debug, Hash)]
955+
#[reflect(PartialEq)]
956+
struct Foo(i32);
957+
958+
impl Debug for Foo {
959+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
960+
write!(f, "Foo")
961+
}
962+
}
963+
964+
let foo = Foo(123);
965+
let foo: &dyn Reflect = &foo;
966+
967+
assert!(foo.reflect_hash().is_some());
968+
assert_eq!(Some(true), foo.reflect_partial_eq(foo));
969+
assert_eq!("Foo".to_string(), format!("{foo:?}"));
970+
}
971+
972+
#[test]
973+
fn multiple_reflect_value_lists() {
974+
#[derive(Clone, Hash, PartialEq, Reflect)]
975+
#[reflect_value(Debug, Hash)]
976+
#[reflect_value(PartialEq)]
977+
struct Foo(i32);
978+
979+
impl Debug for Foo {
980+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
981+
write!(f, "Foo")
982+
}
983+
}
984+
985+
let foo = Foo(123);
986+
let foo: &dyn Reflect = &foo;
987+
988+
assert!(foo.reflect_hash().is_some());
989+
assert_eq!(Some(true), foo.reflect_partial_eq(foo));
990+
assert_eq!("Foo".to_string(), format!("{foo:?}"));
991+
}
992+
951993
#[cfg(feature = "glam")]
952994
mod glam {
953995
use super::*;

0 commit comments

Comments
 (0)