use crate::{custom_attributes::CustomAttributes, derive_data::ReflectTraitToImpl};
use bevy_macro_utils::{
fq_std::{FQAny, FQClone, FQOption, FQResult},
terminated_parser,
};
use proc_macro2::{Ident, Span};
use quote::quote_spanned;
use syn::{
ext::IdentExt, parenthesized, parse::ParseStream, spanned::Spanned, token, Expr, LitBool,
MetaList, MetaNameValue, Path, Token, WhereClause,
};
mod kw {
syn::custom_keyword!(from_reflect);
syn::custom_keyword!(type_path);
syn::custom_keyword!(Debug);
syn::custom_keyword!(PartialEq);
syn::custom_keyword!(Hash);
syn::custom_keyword!(Clone);
syn::custom_keyword!(no_field_bounds);
syn::custom_keyword!(no_auto_register);
syn::custom_keyword!(opaque);
}
const DEBUG_ATTR: &str = "Debug";
const PARTIAL_EQ_ATTR: &str = "PartialEq";
const HASH_ATTR: &str = "Hash";
pub(crate) const REFLECT_DEFAULT: &str = "ReflectDefault";
const FROM_REFLECT_ATTR: &str = "from_reflect";
const TYPE_PATH_ATTR: &str = "type_path";
const CONFLICTING_TYPE_DATA_MESSAGE: &str = "conflicting type data registration";
#[derive(Clone, Default)]
pub(crate) enum TraitImpl {
#[default]
NotImplemented,
Implemented(Span),
Custom(Path, Span),
}
impl TraitImpl {
pub fn merge(&mut self, other: TraitImpl) -> Result<(), syn::Error> {
match (&self, other) {
(TraitImpl::NotImplemented, value) => {
*self = value;
Ok(())
}
(_, TraitImpl::NotImplemented) => Ok(()),
(_, TraitImpl::Implemented(span) | TraitImpl::Custom(_, span)) => {
Err(syn::Error::new(span, CONFLICTING_TYPE_DATA_MESSAGE))
}
}
}
}
#[derive(Clone, Default)]
pub(crate) struct FromReflectAttrs {
auto_derive: Option<LitBool>,
}
impl FromReflectAttrs {
pub fn should_auto_derive(&self) -> bool {
self.auto_derive.as_ref().is_none_or(LitBool::value)
}
}
#[derive(Clone, Default)]
pub(crate) struct TypePathAttrs {
auto_derive: Option<LitBool>,
}
impl TypePathAttrs {
pub fn should_auto_derive(&self) -> bool {
self.auto_derive.as_ref().is_none_or(LitBool::value)
}
}
#[derive(Default, Clone)]
pub(crate) struct ContainerAttributes {
clone: TraitImpl,
debug: TraitImpl,
hash: TraitImpl,
partial_eq: TraitImpl,
from_reflect_attrs: FromReflectAttrs,
type_path_attrs: TypePathAttrs,
custom_where: Option<WhereClause>,
no_field_bounds: bool,
no_auto_register: bool,
custom_attributes: CustomAttributes,
is_opaque: bool,
idents: Vec<Ident>,
}
impl ContainerAttributes {
pub fn parse_terminated(
&mut self,
input: ParseStream,
trait_: ReflectTraitToImpl,
) -> syn::Result<()> {
terminated_parser(Token![,], |stream| {
self.parse_container_attribute(stream, trait_)
})(input)?;
Ok(())
}
pub fn parse_meta_list(
&mut self,
meta: &MetaList,
trait_: ReflectTraitToImpl,
) -> syn::Result<()> {
meta.parse_args_with(|stream: ParseStream| self.parse_terminated(stream, trait_))
}
fn parse_container_attribute(
&mut self,
input: ParseStream,
trait_: ReflectTraitToImpl,
) -> syn::Result<()> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![@]) {
self.custom_attributes.parse_custom_attribute(input)
} else if lookahead.peek(Token![where]) {
self.parse_custom_where(input)
} else if lookahead.peek(kw::from_reflect) {
self.parse_from_reflect(input, trait_)
} else if lookahead.peek(kw::type_path) {
self.parse_type_path(input, trait_)
} else if lookahead.peek(kw::opaque) {
self.parse_opaque(input)
} else if lookahead.peek(kw::no_field_bounds) {
self.parse_no_field_bounds(input)
} else if lookahead.peek(kw::Clone) {
self.parse_clone(input)
} else if lookahead.peek(kw::no_auto_register) {
self.parse_no_auto_register(input)
} else if lookahead.peek(kw::Debug) {
self.parse_debug(input)
} else if lookahead.peek(kw::Hash) {
self.parse_hash(input)
} else if lookahead.peek(kw::PartialEq) {
self.parse_partial_eq(input)
} else if lookahead.peek(Ident::peek_any) {
self.parse_ident(input)
} else {
Err(lookahead.error())
}
}
fn parse_ident(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<Ident>()?;
if input.peek(token::Paren) {
return Err(syn::Error::new(ident.span(), format!(
"only [{DEBUG_ATTR:?}, {PARTIAL_EQ_ATTR:?}, {HASH_ATTR:?}] may specify custom functions",
)));
}
let ident_name = ident.to_string();
let mut reflect_ident = crate::ident::get_reflect_ident(&ident_name);
reflect_ident.set_span(ident.span());
add_unique_ident(&mut self.idents, reflect_ident)?;
Ok(())
}
fn parse_clone(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::Clone>()?;
if input.peek(token::Paren) {
let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.clone.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.clone = TraitImpl::Implemented(ident.span);
}
Ok(())
}
fn parse_debug(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::Debug>()?;
if input.peek(token::Paren) {
let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.debug.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.debug = TraitImpl::Implemented(ident.span);
}
Ok(())
}
fn parse_partial_eq(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::PartialEq>()?;
if input.peek(token::Paren) {
let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.partial_eq.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.partial_eq = TraitImpl::Implemented(ident.span);
}
Ok(())
}
fn parse_hash(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::Hash>()?;
if input.peek(token::Paren) {
let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.hash.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.hash = TraitImpl::Implemented(ident.span);
}
Ok(())
}
fn parse_opaque(&mut self, input: ParseStream) -> syn::Result<()> {
input.parse::<kw::opaque>()?;
self.is_opaque = true;
Ok(())
}
fn parse_no_field_bounds(&mut self, input: ParseStream) -> syn::Result<()> {
input.parse::<kw::no_field_bounds>()?;
self.no_field_bounds = true;
Ok(())
}
fn parse_no_auto_register(&mut self, input: ParseStream) -> syn::Result<()> {
input.parse::<kw::no_auto_register>()?;
self.no_auto_register = true;
Ok(())
}
fn parse_custom_where(&mut self, input: ParseStream) -> syn::Result<()> {
self.custom_where = Some(input.parse()?);
Ok(())
}
fn parse_from_reflect(
&mut self,
input: ParseStream,
trait_: ReflectTraitToImpl,
) -> syn::Result<()> {
let pair = input.parse::<MetaNameValue>()?;
let extracted_bool = extract_bool(&pair.value, |lit| {
if trait_ == ReflectTraitToImpl::FromReflect {
LitBool::new(true, Span::call_site())
} else {
lit.clone()
}
})?;
if let Some(existing) = &self.from_reflect_attrs.auto_derive {
if existing.value() != extracted_bool.value() {
return Err(syn::Error::new(
extracted_bool.span(),
format!("`{FROM_REFLECT_ATTR}` already set to {}", existing.value()),
));
}
} else {
self.from_reflect_attrs.auto_derive = Some(extracted_bool);
}
Ok(())
}
fn parse_type_path(
&mut self,
input: ParseStream,
trait_: ReflectTraitToImpl,
) -> syn::Result<()> {
let pair = input.parse::<MetaNameValue>()?;
let extracted_bool = extract_bool(&pair.value, |lit| {
if trait_ == ReflectTraitToImpl::TypePath {
LitBool::new(true, Span::call_site())
} else {
lit.clone()
}
})?;
if let Some(existing) = &self.type_path_attrs.auto_derive {
if existing.value() != extracted_bool.value() {
return Err(syn::Error::new(
extracted_bool.span(),
format!("`{TYPE_PATH_ATTR}` already set to {}", existing.value()),
));
}
} else {
self.type_path_attrs.auto_derive = Some(extracted_bool);
}
Ok(())
}
pub fn contains(&self, name: &str) -> bool {
self.idents.iter().any(|ident| ident == name)
}
pub fn idents(&self) -> &[Ident] {
&self.idents
}
#[expect(
clippy::wrong_self_convention,
reason = "Method returns `FromReflectAttrs`, does not actually convert data."
)]
pub fn from_reflect_attrs(&self) -> &FromReflectAttrs {
&self.from_reflect_attrs
}
pub fn type_path_attrs(&self) -> &TypePathAttrs {
&self.type_path_attrs
}
pub fn get_hash_impl(&self, bevy_reflect_path: &Path) -> Option<proc_macro2::TokenStream> {
match &self.hash {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
fn reflect_hash(&self) -> #FQOption<u64> {
use ::core::hash::{Hash, Hasher};
let mut hasher = #bevy_reflect_path::utility::reflect_hasher();
Hash::hash(&#FQAny::type_id(self), &mut hasher);
Hash::hash(self, &mut hasher);
#FQOption::Some(Hasher::finish(&hasher))
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
fn reflect_hash(&self) -> #FQOption<u64> {
#FQOption::Some(#impl_fn(self))
}
}),
TraitImpl::NotImplemented => None,
}
}
pub fn get_partial_eq_impl(
&self,
bevy_reflect_path: &Path,
) -> Option<proc_macro2::TokenStream> {
match &self.partial_eq {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption<bool> {
let value = <dyn #bevy_reflect_path::PartialReflect>::try_downcast_ref::<Self>(value);
if let #FQOption::Some(value) = value {
#FQOption::Some(::core::cmp::PartialEq::eq(self, value))
} else {
#FQOption::Some(false)
}
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption<bool> {
#FQOption::Some(#impl_fn(self, value))
}
}),
TraitImpl::NotImplemented => None,
}
}
pub fn get_debug_impl(&self) -> Option<proc_macro2::TokenStream> {
match &self.debug {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
fn debug(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
::core::fmt::Debug::fmt(self, f)
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
fn debug(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
#impl_fn(self, f)
}
}),
TraitImpl::NotImplemented => None,
}
}
pub fn get_clone_impl(&self, bevy_reflect_path: &Path) -> Option<proc_macro2::TokenStream> {
match &self.clone {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
#[inline]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#FQClone::clone(self)))
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
#[inline]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#impl_fn(self)))
}
}),
TraitImpl::NotImplemented => None,
}
}
pub fn custom_attributes(&self) -> &CustomAttributes {
&self.custom_attributes
}
pub fn custom_where(&self) -> Option<&WhereClause> {
self.custom_where.as_ref()
}
pub fn no_field_bounds(&self) -> bool {
self.no_field_bounds
}
#[cfg(feature = "auto_register")]
pub fn no_auto_register(&self) -> bool {
self.no_auto_register
}
pub fn is_opaque(&self) -> bool {
self.is_opaque
}
}
fn add_unique_ident(idents: &mut Vec<Ident>, ident: Ident) -> Result<(), syn::Error> {
let ident_name = ident.to_string();
if idents.iter().any(|i| i == ident_name.as_str()) {
return Err(syn::Error::new(ident.span(), CONFLICTING_TYPE_DATA_MESSAGE));
}
idents.push(ident);
Ok(())
}
fn extract_bool(
value: &Expr,
mut mapper: impl FnMut(&LitBool) -> LitBool,
) -> Result<LitBool, syn::Error> {
match value {
Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(lit),
..
}) => Ok(mapper(lit)),
_ => Err(syn::Error::new(value.span(), "Expected a boolean value")),
}
}