Skip to content

Commit d24745a

Browse files
committed
support syn::ExprCall and syn::ExprClosure in with attribute
1 parent e8ac838 commit d24745a

11 files changed

Lines changed: 236 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
([#532](https://github.com/JelteF/derive_more/pull/532))
1414
- Add support for custom eq functions in `PartialEq`/`Eq` derive.
1515
([#535](https://github.com/JelteF/derive_more/pull/535))
16+
- Support `syn::ExprCall` and `syn::ExprClosure` (function call expressions and
17+
closures) in addition to `syn::Path` in `#[hash(with(...))]`,
18+
`#[partial_eq(with(...))]` and `#[eq(with(...))]` attributes.
1619

1720
### Fixed
1821

impl/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ debug = ["syn/extra-traits", "dep:unicode-ident"]
5757
deref = []
5858
deref_mut = []
5959
display = ["syn/extra-traits", "dep:unicode-ident", "dep:convert_case"]
60-
eq = ["syn/extra-traits", "syn/visit"]
60+
eq = ["syn/extra-traits", "syn/full", "syn/visit"]
6161
error = ["syn/extra-traits"]
6262
from = ["syn/extra-traits"]
6363
from_str = ["syn/full", "syn/visit", "dep:convert_case"]
64-
hash = ["syn/extra-traits", "syn/visit"]
64+
hash = ["syn/extra-traits", "syn/full", "syn/visit"]
6565
index = []
6666
index_mut = []
6767
into = ["syn/extra-traits", "syn/visit-mut"]

impl/doc/eq.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,14 @@ impl PartialEq for Foo {
341341
}
342342
}
343343
```
344+
345+
A closure or a function call expression returning a callable can be used in place of a path too:
346+
347+
```rust
348+
# use derive_more::PartialEq;
349+
#[derive(Debug, PartialEq)]
350+
struct Foo(#[partial_eq(with(|a: &i32, b: &i32| a % 10 == b % 10))] i32);
351+
352+
assert_eq!(Foo(12), Foo(42));
353+
assert_ne!(Foo(12), Foo(13));
354+
```

impl/doc/hash.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,15 @@ impl Hash for Foo {
272272

273273
This is useful for types that don't implement `Hash` but can be hashed in a custom way, or when you need different
274274
hashing behavior than the default.
275+
276+
A closure or a function call expression returning a callable can be used in place of a path too:
277+
278+
```rust
279+
# use derive_more::Hash;
280+
# use core::hash::Hasher;
281+
#[derive(Hash)]
282+
struct Foo {
283+
#[hash(with(|value: &i32, state: &mut _| Hasher::write_i32(state, value.abs())))]
284+
value: i32,
285+
}
286+
```

impl/src/cmp/partial_eq.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
124124
/// Indices of [`syn::Field`]s marked with an [`attr::Skip`].
125125
type SkippedFields = HashSet<usize>;
126126

127-
/// Mapping from [`syn::Field`] marked with an [`attr::With`] to the [`syn::Path`] of the alternate
128-
/// eq function.
129-
type FieldsWithAlternateEqFunction = HashMap<usize, syn::Path>;
127+
/// Mapping from [`syn::Field`] marked with an [`attr::With`] to the alternate eq function
128+
/// expression.
129+
type FieldsWithAlternateEqFunction = HashMap<usize, attr::Callable>;
130130

131131
/// Expansion of a macro for generating a structural [`PartialEq`] implementation of an enum or a
132132
/// struct.
@@ -213,7 +213,7 @@ impl StructuralExpansion<'_> {
213213
.get(&num)
214214
.map(|eq_fn| {
215215
let maybe_not = (!eq).then(|| quote! {!});
216-
quote! { #maybe_not #eq_fn(#self_val, #other_val) }
216+
quote! { #maybe_not (#eq_fn)(#self_val, #other_val) }
217217
}
218218
).unwrap_or_else(|| quote! { #self_val #cmp #other_val }
219219
);

impl/src/hash.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStr
138138
/// Indices of [`syn::Field`]s marked with an [`attr::Skip`].
139139
type SkippedFields = HashSet<usize>;
140140

141-
/// Mapping from [`syn::Field`] marked with an [`attr::With`] to the [`syn::Path`] of the alternate
142-
/// hash function.
143-
type FieldsWithAlternateHashFunction = HashMap<usize, syn::Path>;
141+
/// Mapping from [`syn::Field`] marked with an [`attr::With`] to the alternate hash function
142+
/// expression.
143+
type FieldsWithAlternateHashFunction = HashMap<usize, attr::Callable>;
144144

145145
/// Expansion of a macro for generating a structural [`Hash`] implementation of an enum or a struct.
146146
struct StructuralExpansion<'i> {
@@ -205,9 +205,9 @@ impl StructuralExpansion<'_> {
205205
let self_val = format_ident!("__self_{num}");
206206
let hash_function = alternate_hash_functions
207207
.get(&num)
208-
.map(|it| quote! {#it})
208+
.map(|it| quote! { (#it) })
209209
.unwrap_or_else(
210-
|| quote! {derive_more::core::hash::Hash::hash},
210+
|| quote! { derive_more::core::hash::Hash::hash },
211211
);
212212

213213
punctuated::Pair::Punctuated(

impl/src/utils.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,17 @@ pub(crate) mod attr {
15751575
#[cfg(feature = "try_from")]
15761576
pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt};
15771577

1578+
/// A callable expression accepted as an attribute argument: a function call
1579+
/// (`foo(arg)`), a path to a function (`foo::bar`), or a closure (`|x| ...`).
1580+
#[cfg(any(
1581+
feature = "eq",
1582+
feature = "from_str",
1583+
feature = "hash",
1584+
feature = "try_into",
1585+
))]
1586+
pub(crate) type Callable =
1587+
Either<syn::ExprCall, Either<syn::Path, syn::ExprClosure>>;
1588+
15781589
/// [`Parse`]ing with additional state or metadata.
15791590
pub(crate) trait Parser {
15801591
/// [`Parse`]s an item, using additional state or metadata.
@@ -2173,10 +2184,10 @@ pub(crate) mod attr {
21732184
pub(crate) mod error {
21742185
use syn::parse::{Parse, ParseStream};
21752186

2176-
use super::{Either, ParseMultiple};
2187+
use super::{Callable, ParseMultiple};
21772188

21782189
/// Representation of an attribute, specifying the error type and, optionally, a
2179-
/// [`Conversion`] from a built-in error type.
2190+
/// [`Callable`] conversion from a built-in error type.
21802191
///
21812192
/// ```rust,ignore
21822193
/// #[<attribute>(error(<ty>))]
@@ -2189,7 +2200,7 @@ pub(crate) mod attr {
21892200
/// Custom conversion.
21902201
///
21912202
/// If [`None`], then [`Into`] conversion should be applied.
2192-
pub(crate) conv: Option<Conversion>,
2203+
pub(crate) conv: Option<Callable>,
21932204
}
21942205

21952206
impl Parse for Error {
@@ -2212,7 +2223,7 @@ pub(crate) mod attr {
22122223

22132224
_ = syn::token::Comma::parse(&inner)?;
22142225

2215-
let conv = Conversion::parse(&inner)?;
2226+
let conv = Callable::parse(&inner)?;
22162227
if inner.is_empty() {
22172228
Ok(Self {
22182229
ty,
@@ -2228,12 +2239,6 @@ pub(crate) mod attr {
22282239
}
22292240

22302241
impl ParseMultiple for Error {}
2231-
2232-
/// Possible conversions of an [`attr::Error`].
2233-
///
2234-
/// [`attr::Error`]: Error
2235-
pub(crate) type Conversion =
2236-
Either<syn::ExprCall, Either<syn::Path, syn::ExprClosure>>;
22372242
}
22382243

22392244
#[cfg(feature = "try_from")]
@@ -2411,16 +2416,16 @@ pub(crate) mod attr {
24112416
use syn::parenthesized;
24122417
use syn::parse::{Parse, ParseStream};
24132418

2414-
use crate::utils::attr::ParseMultiple;
2419+
use crate::utils::attr::{Callable, ParseMultiple};
24152420

24162421
/// Representation of an attribute, specifying a custom function for a trait method.
24172422
///
24182423
/// ```rust,ignore
2419-
/// #[<attribute>(with(<path>))]
2424+
/// #[<attribute>(with(<func>))]
24202425
/// ```
24212426
pub(crate) struct With {
24222427
/// Custom function.
2423-
pub(crate) func: syn::Path, // TODO: Support `syn::ExprCall` and `syn::ExprClosure` too.
2428+
pub(crate) func: Callable,
24242429
}
24252430

24262431
impl Parse for With {
@@ -2432,9 +2437,9 @@ pub(crate) mod attr {
24322437
"unknown attribute argument, expected `with(...)` argument here",
24332438
));
24342439
}
2435-
let path_and_parents;
2436-
parenthesized!(path_and_parents in input);
2437-
let func = path_and_parents.parse::<syn::Path>()?;
2440+
let func_tokens;
2441+
parenthesized!(func_tokens in input);
2442+
let func = func_tokens.parse::<Callable>()?;
24382443
Ok(Self { func })
24392444
}
24402445
}

tests/compile_fail/partial_eq/unknown_with_function.stderr

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
error[E0425]: cannot find function `unknown` in this scope
1+
error[E0425]: cannot find value `unknown` in this scope
22
--> tests/compile_fail/partial_eq/unknown_with_function.rs:2:30
33
|
44
2 | struct Foo(#[partial_eq(with(unknown))] i32);
55
| ^^^^^^^ not found in this scope
66

7+
error[E0425]: cannot find value `unknown` in this scope
8+
--> tests/compile_fail/partial_eq/unknown_with_function.rs:11:29
9+
|
10+
11 | Bar { #[partial_eq(with(unknown))] i: i32 },
11+
| ^^^^^^^ not found in this scope
12+
713
error[E0061]: this function takes 1 argument but 2 arguments were supplied
814
--> tests/compile_fail/partial_eq/unknown_with_function.rs:7:30
915
|
@@ -34,12 +40,6 @@ error[E0308]: mismatched types
3440
|
3541
= note: this error originates in the derive macro `derive_more::PartialEq` (in Nightly builds, run with -Z macro-backtrace for more info)
3642

37-
error[E0425]: cannot find function `unknown` in this scope
38-
--> tests/compile_fail/partial_eq/unknown_with_function.rs:11:29
39-
|
40-
11 | Bar { #[partial_eq(with(unknown))] i: i32 },
41-
| ^^^^^^^ not found in this scope
42-
4343
error[E0061]: this function takes 1 argument but 2 arguments were supplied
4444
--> tests/compile_fail/partial_eq/unknown_with_function.rs:12:29
4545
|

tests/eq.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,37 @@ mod structs {
218218

219219
let _: AssertParamIsEq<Bar>;
220220
}
221+
222+
#[test]
223+
fn closure() {
224+
#[derive(Eq, PartialEq)]
225+
struct Foo(
226+
#[partial_eq(with(|_: &NotPartialEq, _: &NotPartialEq| true))]
227+
NotPartialEq,
228+
);
229+
#[derive(Eq, PartialEq)]
230+
struct Bar(
231+
#[eq(with(|_: &NotPartialEq, _: &NotPartialEq| true))] NotPartialEq,
232+
);
233+
234+
let _: AssertParamIsEq<Foo>;
235+
let _: AssertParamIsEq<Bar>;
236+
}
237+
238+
#[test]
239+
fn fn_call() {
240+
fn make_eq_fn() -> fn(&NotPartialEq, &NotPartialEq) -> bool {
241+
|_, _| true
242+
}
243+
244+
#[derive(Eq, PartialEq)]
245+
struct Foo(#[partial_eq(with(make_eq_fn()))] NotPartialEq);
246+
#[derive(Eq, PartialEq)]
247+
struct Bar(#[eq(with(make_eq_fn()))] NotPartialEq);
248+
249+
let _: AssertParamIsEq<Foo>;
250+
let _: AssertParamIsEq<Bar>;
251+
}
221252
}
222253

223254
mod generic {
@@ -664,6 +695,40 @@ mod enums {
664695
let _: AssertParamIsEq<E>;
665696
}
666697

698+
#[test]
699+
fn closure() {
700+
#[derive(Eq, PartialEq)]
701+
enum E {
702+
Foo(
703+
#[partial_eq(with(|_: &NotPartialEq, _: &NotPartialEq| true))]
704+
NotPartialEq,
705+
),
706+
Bar(
707+
#[eq(with(|_: &NotPartialEq, _: &NotPartialEq| true))]
708+
NotPartialEq,
709+
),
710+
Baz,
711+
}
712+
713+
let _: AssertParamIsEq<E>;
714+
}
715+
716+
#[test]
717+
fn fn_call() {
718+
fn make_eq_fn() -> fn(&NotPartialEq, &NotPartialEq) -> bool {
719+
|_, _| true
720+
}
721+
722+
#[derive(Eq, PartialEq)]
723+
enum E {
724+
Foo(#[partial_eq(with(make_eq_fn()))] NotPartialEq),
725+
Bar(#[eq(with(make_eq_fn()))] NotPartialEq),
726+
Baz,
727+
}
728+
729+
let _: AssertParamIsEq<E>;
730+
}
731+
667732
#[test]
668733
fn multi_variant() {
669734
#[derive(Eq, PartialEq)]

tests/hash.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ pub mod utils {
3939
state.write_u32(42);
4040
state.write_u32(*value)
4141
}
42+
43+
pub fn make_u32_hash_function() -> fn(&u32, &mut dyn core::hash::Hasher) {
44+
|value, state| {
45+
state.write_u32(1337);
46+
state.write_u32(*value);
47+
}
48+
}
4249
}
4350

4451
mod structs {
@@ -99,6 +106,23 @@ mod structs {
99106
c: bool,
100107
}
101108

109+
#[derive(Hash)]
110+
struct StructWithClosureHashFunction {
111+
#[hash(with(|value: &u32, state: &mut _| {
112+
core::hash::Hasher::write_u32(state, 42);
113+
core::hash::Hasher::write_u32(state, *value);
114+
}))]
115+
a: u32,
116+
b: &'static str,
117+
}
118+
119+
#[derive(Hash)]
120+
struct StructWithCallExprHashFunction {
121+
#[hash(with(utils::make_u32_hash_function()))]
122+
a: u32,
123+
b: &'static str,
124+
}
125+
102126
#[derive(Hash)]
103127
struct MixedSkip {
104128
field1: i32,
@@ -135,6 +159,14 @@ mod structs {
135159
}),
136160
do_hash(&(42, 42, "test", true))
137161
);
162+
assert_eq!(
163+
do_hash(&StructWithClosureHashFunction { a: 42, b: "test" }),
164+
do_hash(&(42, 42, "test"))
165+
);
166+
assert_eq!(
167+
do_hash(&StructWithCallExprHashFunction { a: 42, b: "test" }),
168+
do_hash(&(1337, 42, "test"))
169+
);
138170
assert_eq!(
139171
do_hash(&MixedSkip {
140172
field1: 42,
@@ -231,6 +263,14 @@ mod enums {
231263
#[hash(skip)]
232264
#[allow(unused)]
233265
C(i32),
266+
D(
267+
#[hash(with(|value: &u32, state: &mut _| {
268+
core::hash::Hasher::write_u32(state, 7);
269+
core::hash::Hasher::write_u32(state, *value);
270+
}))]
271+
u32,
272+
),
273+
E(#[hash(with(utils::make_u32_hash_function()))] u32),
234274
}
235275

236276
#[test]
@@ -294,6 +334,18 @@ mod enums {
294334

295335
let wc = WithAndSkip::C(42);
296336
assert_eq!(do_hash(&wc), do_hash(&core::mem::discriminant(&wc)));
337+
338+
let wd = WithAndSkip::D(42);
339+
assert_eq!(
340+
do_hash(&wd),
341+
do_hash(&(core::mem::discriminant(&wd), 7, 42)),
342+
);
343+
344+
let we = WithAndSkip::E(42);
345+
assert_eq!(
346+
do_hash(&we),
347+
do_hash(&(core::mem::discriminant(&we), 1337, 42)),
348+
);
297349
}
298350
}
299351

0 commit comments

Comments
 (0)