Skip to content

Commit

Permalink
bevy_derive: Add #[deref] attribute (#8552)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
MrGVSV authored May 16, 2023
1 parent 1da726e commit 56686a8
Show file tree
Hide file tree
Showing 33 changed files with 696 additions and 22 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rust-version = "1.67.0"
exclude = [
"benches",
"crates/bevy_ecs_compile_fail_tests",
"crates/bevy_macros_compile_fail_tests",
"crates/bevy_reflect_compile_fail_tests",
]
members = [
Expand Down
72 changes: 57 additions & 15 deletions crates/bevy_derive/src/derefs.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Index, Member, Type};
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_inner_field(&ast, false) {
let (field_member, field_type) = match get_deref_field(&ast, false) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
Expand All @@ -29,7 +33,7 @@ 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_inner_field(&ast, true) {
let (field_member, _) = match get_deref_field(&ast, true) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
Expand All @@ -46,24 +50,62 @@ pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
})
}

fn get_inner_field(ast: &DeriveInput, is_mut: bool) -> syn::Result<(Member, &Type)> {
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 = field
.ident
.as_ref()
.map(|name| Member::Named(name.clone()))
.unwrap_or_else(|| Member::Unnamed(Index::from(0)));
let member = to_member(field, 0);
Ok((member, &field.ty))
}
_ => {
let msg = if is_mut {
"DerefMut can only be derived for structs with a single field"
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 {
"Deref can only be derived for structs with a single field"
};
Err(syn::Error::new(Span::call_site().into(), msg))
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)))
}
131 changes: 124 additions & 7 deletions crates/bevy_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,104 @@ pub fn derive_dynamic_plugin(input: TokenStream) -> TokenStream {
app_plugin::derive_dynamic_plugin(input)
}

/// Implements [`Deref`] for _single-item_ structs. This is especially useful when
/// utilizing the [newtype] pattern.
/// Implements [`Deref`] for structs. This is especially useful when utilizing the [newtype] pattern.
///
/// For single-field structs, the implementation automatically uses that field.
/// For multi-field structs, you must specify which field to use with the `#[deref]` attribute.
///
/// If you need [`DerefMut`] as well, consider using the other [derive] macro alongside
/// this one.
///
/// # Example
///
/// ## Tuple Structs
///
/// Using a single-field struct:
///
/// ```
/// use bevy_derive::Deref;
///
/// #[derive(Deref)]
/// struct MyNewtype(String);
///
/// let foo = MyNewtype(String::from("Hello"));
/// assert_eq!(5, foo.len());
/// assert_eq!("Hello", *foo);
/// ```
///
/// Using a multi-field struct:
///
/// ```
/// # use std::marker::PhantomData;
/// use bevy_derive::Deref;
///
/// #[derive(Deref)]
/// struct MyStruct<T>(#[deref] String, PhantomData<T>);
///
/// let foo = MyStruct(String::from("Hello"), PhantomData::<usize>);
/// assert_eq!("Hello", *foo);
/// ```
///
/// ## Named Structs
///
/// Using a single-field struct:
///
/// ```
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyStruct {
/// value: String,
/// }
///
/// let foo = MyStruct {
/// value: String::from("Hello")
/// };
/// assert_eq!("Hello", *foo);
/// ```
///
/// Using a multi-field struct:
///
/// ```
/// # use std::marker::PhantomData;
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyStruct<T> {
/// #[deref]
/// value: String,
/// _phantom: PhantomData<T>,
/// }
///
/// let foo = MyStruct {
/// value:String::from("Hello"),
/// _phantom:PhantomData::<usize>
/// };
/// assert_eq!("Hello", *foo);
/// ```
///
/// [`Deref`]: std::ops::Deref
/// [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
/// [`DerefMut`]: std::ops::DerefMut
/// [derive]: crate::derive_deref_mut
#[proc_macro_derive(Deref)]
#[proc_macro_derive(Deref, attributes(deref))]
pub fn derive_deref(input: TokenStream) -> TokenStream {
derefs::derive_deref(input)
}

/// Implements [`DerefMut`] for _single-item_ structs. This is especially useful when
/// utilizing the [newtype] pattern.
/// Implements [`DerefMut`] for structs. This is especially useful when utilizing the [newtype] pattern.
///
/// For single-field structs, the implementation automatically uses that field.
/// For multi-field structs, you must specify which field to use with the `#[deref]` attribute.
///
/// [`DerefMut`] requires a [`Deref`] implementation. You can implement it manually or use
/// Bevy's [derive] macro for convenience.
///
/// # Example
///
/// ## Tuple Structs
///
/// Using a single-field struct:
///
/// ```
/// use bevy_derive::{Deref, DerefMut};
///
Expand All @@ -63,11 +126,65 @@ pub fn derive_deref(input: TokenStream) -> TokenStream {
/// assert_eq!("Hello World!", *foo);
/// ```
///
/// Using a multi-field struct:
///
/// ```
/// # use std::marker::PhantomData;
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyStruct<T>(#[deref] String, PhantomData<T>);
///
/// let mut foo = MyStruct(String::from("Hello"), PhantomData::<usize>);
/// foo.push_str(" World!");
/// assert_eq!("Hello World!", *foo);
/// ```
///
/// ## Named Structs
///
/// Using a single-field struct:
///
/// ```
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyStruct {
/// value: String,
/// }
///
/// let mut foo = MyStruct {
/// value: String::from("Hello")
/// };
/// foo.push_str(" World!");
/// assert_eq!("Hello World!", *foo);
/// ```
///
/// Using a multi-field struct:
///
/// ```
/// # use std::marker::PhantomData;
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyStruct<T> {
/// #[deref]
/// value: String,
/// _phantom: PhantomData<T>,
/// }
///
/// let mut foo = MyStruct {
/// value:String::from("Hello"),
/// _phantom:PhantomData::<usize>
/// };
/// foo.push_str(" World!");
/// assert_eq!("Hello World!", *foo);
/// ```
///
/// [`DerefMut`]: std::ops::DerefMut
/// [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
/// [`Deref`]: std::ops::Deref
/// [derive]: crate::derive_deref
#[proc_macro_derive(DerefMut)]
#[proc_macro_derive(DerefMut, attributes(deref))]
pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
derefs::derive_deref_mut(input)
}
Expand Down
13 changes: 13 additions & 0 deletions crates/bevy_macros_compile_fail_tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "bevy_macros_compile_fail_tests"
version = "0.1.0"
edition = "2021"
description = "Compile fail tests for Bevy Engine's various macros"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
publish = false

[dependencies]
bevy_derive = { path = "../bevy_derive" }
trybuild = "1.0.71"
6 changes: 6 additions & 0 deletions crates/bevy_macros_compile_fail_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Compile fail tests for Bevy macros

This crate is not part of the Bevy workspace in order to not fail `crater` tests for Bevy.
The tests assert on the exact compiler errors and can easily fail for new Rust versions due to updated compiler errors (e.g. changes in spans).

The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../tools/ci/src/main.rs)).
1 change: 1 addition & 0 deletions crates/bevy_macros_compile_fail_tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Nothing here, check out the integration tests
6 changes: 6 additions & 0 deletions crates/bevy_macros_compile_fail_tests/tests/deref_derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[test]
fn test() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/deref_derive/*.fail.rs");
t.pass("tests/deref_derive/*.pass.rs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use bevy_derive::Deref;

// Reason: `#[deref]` doesn't take any arguments

#[derive(Deref)]
struct TupleStruct(usize, #[deref()] String);

#[derive(Deref)]
struct Struct {
foo: usize,
#[deref()]
bar: String,
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: unexpected token in attribute
--> tests/deref_derive/invalid_attribute.fail.rs:6:34
|
6 | struct TupleStruct(usize, #[deref()] String);
| ^

error: unexpected token in attribute
--> tests/deref_derive/invalid_attribute.fail.rs:11:12
|
11 | #[deref()]
| ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use bevy_derive::Deref;

#[derive(Deref)]
struct UnitStruct;

#[derive(Deref)]
enum Enum {}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error: Deref cannot be derived on field-less structs
--> tests/deref_derive/invalid_item.fail.rs:3:10
|
3 | #[derive(Deref)]
| ^^^^^
|
= note: this error originates in the derive macro `Deref` (in Nightly builds, run with -Z macro-backtrace for more info)

error: Deref can only be derived on structs
--> tests/deref_derive/invalid_item.fail.rs:6:10
|
6 | #[derive(Deref)]
| ^^^^^
|
= note: this error originates in the derive macro `Deref` (in Nightly builds, run with -Z macro-backtrace for more info)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use bevy_derive::Deref;

#[derive(Deref)]
struct TupleStruct(usize, String);

#[derive(Deref)]
struct Struct {
foo: usize,
bar: String,
}

fn main() {}
Loading

0 comments on commit 56686a8

Please sign in to comment.