bevy/crates/bevy_derive/src/derefs.rs
Gino Valente 56686a8962
bevy_derive: Add #[deref] attribute (#8552)
# Objective

Bevy code tends to make heavy use of the [newtype](
https://doc.rust-lang.org/rust-by-example/generics/new_types.html)
pattern, which is why we have a dedicated derive for
[`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) and
[`DerefMut`](https://doc.rust-lang.org/std/ops/trait.DerefMut.html).
This derive works for any struct with a single field:

```rust
#[derive(Component, Deref, DerefMut)]
struct MyNewtype(usize);
```

One reason for the single-field limitation is to prevent confusion and
footguns related that would arise from allowing multi-field structs:

<table align="center">
<tr>
<th colspan="2">
Similar structs, different derefs
</th>
</tr>
<tr>
<td>

```rust
#[derive(Deref, DerefMut)]
struct MyStruct {
  foo: usize, // <- Derefs usize
  bar: String,
}
```

</td>
<td>

```rust
#[derive(Deref, DerefMut)]
struct MyStruct {
  bar: String, // <- Derefs String
  foo: usize,
}
```

</td>
</tr>
<tr>
<th colspan="2">
Why `.1`?
</th>
</tr>
<tr>
<td colspan="2">

```rust
#[derive(Deref, DerefMut)]
struct MyStruct(Vec<usize>, Vec<f32>);

let mut foo = MyStruct(vec![123], vec![1.23]);

// Why can we skip the `.0` here?
foo.push(456);
// But not here?
foo.1.push(4.56);
```

</td>
</tr>
</table>

However, there are certainly cases where it's useful to allow for
structs with multiple fields. Such as for structs with one "real" field
and one `PhantomData` to allow for generics:

```rust
#[derive(Deref, DerefMut)]
struct MyStruct<T>(
  // We want use this field for the `Deref`/`DerefMut` impls
  String,
  // But we need this field so that we can make this struct generic
  PhantomData<T>
);

// ERROR: Deref can only be derived for structs with a single field
// ERROR: DerefMut can only be derived for structs with a single field
```

Additionally, the possible confusion and footguns are mainly an issue
for newer Rust/Bevy users. Those familiar with `Deref` and `DerefMut`
understand what adding the derive really means and can anticipate its
behavior.

## Solution

Allow users to opt into multi-field `Deref`/`DerefMut` derives using a
`#[deref]` attribute:

```rust
#[derive(Deref, DerefMut)]
struct MyStruct<T>(
  // Use this field for the `Deref`/`DerefMut` impls
  #[deref] String,
  // We can freely include any other field without a compile error
  PhantomData<T>
);
```

This prevents the footgun pointed out in the first issue described in
the previous section, but it still leaves the possible confusion
surrounding `.0`-vs-`.#`. However, the idea is that by making this
behavior explicit with an attribute, users will be more aware of it and
can adapt appropriately.

---

## Changelog

- Added `#[deref]` attribute to `Deref` and `DerefMut` derives
2023-05-16 18:29:09 +00:00

112 lines
3.8 KiB
Rust

use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Field, Index, Member, Type};
const DEREF: &str = "Deref";
const DEREF_MUT: &str = "DerefMut";
const DEREF_ATTR: &str = "deref";
pub fn derive_deref(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ident = &ast.ident;
let (field_member, field_type) = match get_deref_field(&ast, false) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
}
};
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics ::std::ops::Deref for #ident #ty_generics #where_clause {
type Target = #field_type;
fn deref(&self) -> &Self::Target {
&self.#field_member
}
}
})
}
pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ident = &ast.ident;
let (field_member, _) = match get_deref_field(&ast, true) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
}
};
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics ::std::ops::DerefMut for #ident #ty_generics #where_clause {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.#field_member
}
}
})
}
fn get_deref_field(ast: &DeriveInput, is_mut: bool) -> syn::Result<(Member, &Type)> {
let deref_kind = if is_mut { DEREF_MUT } else { DEREF };
let deref_attr_str = format!("`#[{DEREF_ATTR}]`");
match &ast.data {
Data::Struct(data_struct) if data_struct.fields.is_empty() => Err(syn::Error::new(
Span::call_site().into(),
format!("{deref_kind} cannot be derived on field-less structs"),
)),
Data::Struct(data_struct) if data_struct.fields.len() == 1 => {
let field = data_struct.fields.iter().next().unwrap();
let member = to_member(field, 0);
Ok((member, &field.ty))
}
Data::Struct(data_struct) => {
let mut selected_field: Option<(Member, &Type)> = None;
for (index, field) in data_struct.fields.iter().enumerate() {
for attr in &field.attrs {
if !attr.meta.require_path_only()?.is_ident(DEREF_ATTR) {
continue;
}
if selected_field.is_some() {
return Err(syn::Error::new_spanned(
attr,
format!(
"{deref_attr_str} attribute can only be used on a single field"
),
));
}
let member = to_member(field, index);
selected_field = Some((member, &field.ty));
}
}
if let Some(selected_field) = selected_field {
Ok(selected_field)
} else {
Err(syn::Error::new(
Span::call_site().into(),
format!("deriving {deref_kind} on multi-field structs requires one field to have the {deref_attr_str} attribute"),
))
}
}
_ => Err(syn::Error::new(
Span::call_site().into(),
format!("{deref_kind} can only be derived on structs"),
)),
}
}
fn to_member(field: &Field, index: usize) -> Member {
field
.ident
.as_ref()
.map(|name| Member::Named(name.clone()))
.unwrap_or_else(|| Member::Unnamed(Index::from(index)))
}