Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - bevy_reflect_derive: Tidying up the code #4712

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/bevy_macro_utils/src/lib.rs
Expand Up @@ -78,6 +78,22 @@ impl BevyManifest {
deps.and_then(find_in_deps)
.or_else(|| deps_dev.and_then(find_in_deps))
}

/// Returns the path for the crate with the given name.
///
/// This is a convenience method for constructing a [manifest] and
/// calling the [`get_path`] method.
///
/// This method should only be used where you just need the path and can't
/// cache the [manifest]. If caching is possible, it's recommended to create
/// the [manifest] yourself and use the [`get_path`] method.
///
/// [`get_path`]: Self::get_path
/// [manifest]: Self
pub fn get_path_direct(name: &str) -> syn::Path {
Self::default().get_path(name)
}

pub fn get_path(&self, name: &str) -> syn::Path {
self.maybe_get_path(name)
.unwrap_or_else(|| Self::parse_str(name))
Expand Down
234 changes: 234 additions & 0 deletions crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs
@@ -0,0 +1,234 @@
//! Contains code related to container attributes for reflected types.
//!
//! A container attribute is an attribute which applies to an entire struct or enum
//! as opposed to a particular field or variant. An example of such an attribute is
//! the derive helper attribute for `Reflect`, which looks like:
//! `#[reflect(PartialEq, Default, ...)]` and `#[reflect_value(PartialEq, Default, ...)]`.

use crate::utility;
use proc_macro2::Ident;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Meta, NestedMeta, Path};

// The "special" trait idents that are used internally for reflection.
// Received via attributes like `#[reflect(PartialEq, Hash, ...)]`
const PARTIAL_EQ_ATTR: &str = "PartialEq";
const HASH_ATTR: &str = "Hash";
const SERIALIZE_ATTR: &str = "Serialize";

/// A marker for trait implementations registered via the `Reflect` derive macro.
#[derive(Clone)]
pub(crate) enum TraitImpl {
/// The trait is not registered as implemented.
NotImplemented,
/// The trait is registered as implemented.
Implemented,

// TODO: This can be made to use `ExprPath` instead of `Ident`, allowing for fully qualified paths to be used
/// The trait is registered with a custom function rather than an actual implementation.
Custom(Ident),
MrGVSV marked this conversation as resolved.
Show resolved Hide resolved
}

impl Default for TraitImpl {
fn default() -> Self {
Self::NotImplemented
}
}

/// A collection of traits that have been registered for a reflected type.
///
/// This keeps track of a few traits that are utilized internally for reflection
/// (we'll call these traits _special traits_ within this context), but it
/// will also keep track of all registered traits. Traits are registered as part of the
/// `Reflect` derive macro using the helper attribute: `#[reflect(...)]`.
///
/// The list of special traits are as follows:
/// * `Hash`
/// * `PartialEq`
/// * `Serialize`
///
/// When registering a trait, there are a few things to keep in mind:
/// * Traits must have a valid `Reflect{}` struct in scope. For example, `Default`
/// needs `bevy_reflect::prelude::ReflectDefault` in scope.
/// * Traits must be single path identifiers. This means you _must_ use `Default`
/// instead of `std::default::Default` (otherwise it will try to register `Reflectstd`!)
/// * A custom function may be supplied in place of an actual implementation
/// for the special traits (but still follows the same single-path identifier
/// rules as normal).
///
/// # Example
///
/// Registering the `Default` implementation:
///
/// ```ignore
/// // Import ReflectDefault so it's accessible by the derive macro
/// use bevy_reflect::prelude::ReflectDefault.
///
/// #[derive(Reflect, Default)]
/// #[reflect(Default)]
/// struct Foo;
/// ```
///
/// Registering the `Hash` implementation:
///
/// ```ignore
/// // `Hash` is a "special trait" and does not need (nor have) a ReflectHash struct
///
/// #[derive(Reflect, Hash)]
/// #[reflect(Hash)]
/// struct Foo;
/// ```
///
/// Registering the `Hash` implementation using a custom function:
///
/// ```ignore
/// // This function acts as our `Hash` implementation and
/// // corresponds to the `Reflect::reflect_hash` method.
/// fn get_hash(foo: &Foo) -> Option<u64> {
/// Some(123)
/// }
///
/// #[derive(Reflect)]
/// // Register the custom `Hash` function
/// #[reflect(Hash(get_hash))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it would make sense to add Default as a special trait- it definitely has special behavior, and allowing a special custom function for it like this would be nice.
Outside of the scope of this PR, but occurred to me nonetheless.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. It would involve adding a Reflect::reflect_default() method or something though. "Special traits" aren't stored as type data like other traits (can't access it via the registry). So we either add that method to Reflect and remove ReflectDefault or we make all "special traits" accessible from the registry as well.

But yeah that's probably best for a follow up PR (maybe even in #4140).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this in #4140 at first glance.

/// struct Foo;
/// ```
///
/// > __Note:__ Registering a custom function only works for special traits.
///
#[derive(Default)]
pub(crate) struct ReflectTraits {
hash: TraitImpl,
partial_eq: TraitImpl,
serialize: TraitImpl,
idents: Vec<Ident>,
}

impl ReflectTraits {
/// Create a new [`ReflectTraits`] instance from a set of nested metas.
pub fn from_nested_metas(nested_metas: &Punctuated<NestedMeta, Comma>) -> Self {
let mut traits = ReflectTraits::default();
for nested_meta in nested_metas.iter() {
match nested_meta {
// Handles `#[reflect( Hash, Default, ... )]`
NestedMeta::Meta(Meta::Path(path)) => {
// Get the first ident in the path (hopefully the path only contains one and not `std::hash::Hash`)
let ident = if let Some(segment) = path.segments.iter().next() {
segment.ident.to_string()
} else {
continue;
};

match ident.as_str() {
PARTIAL_EQ_ATTR => traits.partial_eq = TraitImpl::Implemented,
HASH_ATTR => traits.hash = TraitImpl::Implemented,
SERIALIZE_ATTR => traits.serialize = TraitImpl::Implemented,
// We only track reflected idents for traits not considered special
_ => traits.idents.push(utility::get_reflect_ident(&ident)),
}
}
// Handles `#[reflect( Hash(custom_hash_fn) )]`
NestedMeta::Meta(Meta::List(list)) => {
// Get the first ident in the path (hopefully the path only contains one and not `std::hash::Hash`)
let ident = if let Some(segment) = list.path.segments.iter().next() {
segment.ident.to_string()
} else {
continue;
};

let list_meta = list.nested.iter().next();
if let Some(NestedMeta::Meta(Meta::Path(path))) = list_meta {
if let Some(segment) = path.segments.iter().next() {
// This should be the ident of the custom function
let trait_func_ident = TraitImpl::Custom(segment.ident.clone());
match ident.as_str() {
PARTIAL_EQ_ATTR => traits.partial_eq = trait_func_ident,
HASH_ATTR => traits.hash = trait_func_ident,
SERIALIZE_ATTR => traits.serialize = trait_func_ident,
_ => {}
}
}
}
}
_ => {}
}
}

traits
}

/// Returns true if the given reflected trait name (i.e. `ReflectDefault` for `Default`)
/// is registered for this type.
pub fn contains(&self, name: &str) -> bool {
self.idents.iter().any(|ident| ident == name)
}

/// The list of reflected traits by their reflected ident (i.e. `ReflectDefault` for `Default`).
pub fn idents(&self) -> &[Ident] {
&self.idents
}

/// Returns the logic for `Reflect::reflect_hash` as a `TokenStream`.
///
/// If `Hash` was not registered, returns `None`.
pub fn get_hash_impl(&self, path: &Path) -> Option<proc_macro2::TokenStream> {
match &self.hash {
TraitImpl::Implemented => Some(quote! {
use std::hash::{Hash, Hasher};
let mut hasher = #path::ReflectHasher::default();
Hash::hash(&std::any::Any::type_id(self), &mut hasher);
Hash::hash(self, &mut hasher);
Some(hasher.finish())
}),
TraitImpl::Custom(impl_fn) => Some(quote! {
Some(#impl_fn(self))
}),
TraitImpl::NotImplemented => None,
}
}

/// Returns the logic for `Reflect::reflect_partial_eq` as a `TokenStream`.
///
/// If `PartialEq` was not registered, returns `None`.
pub fn get_partial_eq_impl(&self) -> Option<proc_macro2::TokenStream> {
match &self.partial_eq {
TraitImpl::Implemented => Some(quote! {
let value = value.any();
if let Some(value) = value.downcast_ref::<Self>() {
Some(std::cmp::PartialEq::eq(self, value))
} else {
Some(false)
}
}),
TraitImpl::Custom(impl_fn) => Some(quote! {
Some(#impl_fn(self, value))
}),
TraitImpl::NotImplemented => None,
}
}

/// Returns the logic for `Reflect::serializable` as a `TokenStream`.
///
/// If `Serialize` was not registered, returns `None`.
pub fn get_serialize_impl(&self, path: &Path) -> Option<proc_macro2::TokenStream> {
match &self.serialize {
TraitImpl::Implemented => Some(quote! {
Some(#path::serde::Serializable::Borrowed(self))
}),
TraitImpl::Custom(impl_fn) => Some(quote! {
Some(#impl_fn(self))
}),
TraitImpl::NotImplemented => None,
}
}
}

impl Parse for ReflectTraits {
fn parse(input: ParseStream) -> syn::Result<Self> {
let result = Punctuated::<NestedMeta, Comma>::parse_terminated(input)?;
Ok(ReflectTraits::from_nested_metas(&result))
}
}