From 98ce6f2ae44a3e55bed7564f75bbc4be7fcc6231 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Tue, 6 Jan 2026 15:35:12 +0100 Subject: [PATCH 01/12] add .idea to gitignore (rustrover and other jetbrains ide files) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 59b51f17..baf66e28 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ gh-pages # git files .swp /tags + +# RustRover/Other jetbrains IDE files +.idea/ From cd7f01a8dbf9cd35364071872af580fc2ce4aa3d Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Tue, 6 Jan 2026 19:34:20 +0100 Subject: [PATCH 02/12] first version of Hash derive, supporting skipping fields --- Cargo.toml | 7 ++ impl/Cargo.toml | 2 + impl/doc/hash.md | 0 impl/src/hash.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++++ impl/src/lib.rs | 4 + impl/src/utils.rs | 17 +++- src/lib.rs | 5 + tests/hash.rs | 166 +++++++++++++++++++++++++++++++++ 8 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 impl/doc/hash.md create mode 100644 impl/src/hash.rs create mode 100644 tests/hash.rs diff --git a/Cargo.toml b/Cargo.toml index 300ed19e..5e9c4cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ eq = ["derive_more-impl/eq"] error = ["derive_more-impl/error"] from = ["derive_more-impl/from"] from_str = ["derive_more-impl/from_str"] +hash = ["derive_more-impl/hash"] index = ["derive_more-impl/index"] index_mut = ["derive_more-impl/index_mut"] into = ["derive_more-impl/into"] @@ -93,6 +94,7 @@ full = [ "error", "from", "from_str", + "hash", "index", "index_mut", "into", @@ -180,6 +182,11 @@ name = "from_str" path = "tests/from_str.rs" required-features = ["from_str"] +[[test]] +name = "hash" +path = "tests/hash.rs" +required-features = ["hash"] + [[test]] name = "index_mut" path = "tests/index_mut.rs" diff --git a/impl/Cargo.toml b/impl/Cargo.toml index fcabe14c..3cac43c8 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -61,6 +61,7 @@ eq = ["syn/extra-traits", "syn/visit"] error = ["syn/extra-traits"] from = ["syn/extra-traits"] from_str = ["syn/full", "syn/visit", "dep:convert_case"] +hash = ["syn/extra-traits", "syn/visit"] index = [] index_mut = [] into = ["syn/extra-traits", "syn/visit-mut"] @@ -88,6 +89,7 @@ full = [ "error", "from", "from_str", + "hash", "index", "index_mut", "into", diff --git a/impl/doc/hash.md b/impl/doc/hash.md new file mode 100644 index 00000000..e69de29b diff --git a/impl/src/hash.rs b/impl/src/hash.rs new file mode 100644 index 00000000..d21a5064 --- /dev/null +++ b/impl/src/hash.rs @@ -0,0 +1,227 @@ +//! Implementation of an [`Hash`] derive macro. + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse_quote, + punctuated::{self, Punctuated}, + spanned::Spanned as _, +}; + +use crate::utils::{ + attr::{self, ParseMultiple as _}, + pattern_matching::FieldsExt as _, + structural_inclusion::TypeExt as _, + GenericsSearch, HashSet, +}; + +/// Expands a [`Hash`] derive macro. +pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { + let attr_name = format_ident!("hash"); + let secondary_attr_name = format_ident!("eq"); + let tertiary_attr_name = format_ident!("partial_eq"); + + let mut has_skipped_variants = false; + let mut variants = vec![]; + + match &input.data { + syn::Data::Struct(data) => { + for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + if attr::Skip::parse_attrs(&input.attrs, attr_name)?.is_some() { + has_skipped_variants = true; + break; + } + } + if !has_skipped_variants { + let mut skipped_fields = SkippedFields::default(); + 'fields: for (n, field) in data.fields.iter().enumerate() { + for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + if attr::Skip::parse_attrs(&field.attrs, attr_name)?.is_some() { + _ = skipped_fields.insert(n); + continue 'fields; + } + } + } + variants.push((None, &data.fields, skipped_fields)); + } + } + syn::Data::Enum(data) => { + 'variants: for variant in &data.variants { + for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + if attr::Skip::parse_attrs(&variant.attrs, attr_name)?.is_some() { + has_skipped_variants = true; + continue 'variants; + } + } + let mut skipped_fields = SkippedFields::default(); + 'fields: for (n, field) in variant.fields.iter().enumerate() { + for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + if attr::Skip::parse_attrs(&field.attrs, attr_name)?.is_some() { + _ = skipped_fields.insert(n); + continue 'fields; + } + } + } + variants.push((Some(&variant.ident), &variant.fields, skipped_fields)); + } + } + syn::Data::Union(data) => { + return Err(syn::Error::new( + data.union_token.span(), + "`Hash` cannot be derived for unions", + )) + } + } + + Ok(StructuralExpansion { + self_ty: (&input.ident, &input.generics), + variants, + has_skipped_variants, + is_enum: matches!(input.data, syn::Data::Enum(_)), + } + .into_token_stream()) +} + +/// Indices of [`syn::Field`]s marked with an [`attr::Skip`]. +type SkippedFields = HashSet; + +/// Expansion of a macro for generating a structural [`Hash`] implementation of an enum or a +/// struct. +struct StructuralExpansion<'i> { + /// [`syn::Ident`] and [`syn::Generics`] of the enum/struct. + /// + /// [`syn::Ident`]: struct@syn::Ident + self_ty: (&'i syn::Ident, &'i syn::Generics), + + /// [`syn::Fields`] of the enum/struct to be compared in this [`StructuralExpansion`]. + variants: Vec<(Option<&'i syn::Ident>, &'i syn::Fields, SkippedFields)>, + + /// Indicator whether some original enum variants where skipped with an [`attr::Skip`]. + has_skipped_variants: bool, + + /// Indicator whether this expansion is for an enum. + is_enum: bool, +} + +impl StructuralExpansion<'_> { + /// Generates body of the [`Hash::hash()`] method implementation for this + /// [`StructuralExpansion`], if it's required. + fn body(&self) -> TokenStream { + let no_op_body = quote! { }; + + // Special case: empty enum. + if self.is_enum && self.variants.is_empty() && !self.has_skipped_variants { + return no_op_body; + } + + // Special case: fully skipped struct. + if !self.is_enum && self.variants.is_empty() && self.has_skipped_variants { + return no_op_body; + } + // Special case: no fields to hash in struct/single-variant enum. + if !(self.is_enum && self.has_skipped_variants) + && self.variants.len() == 1 + && (self.variants[0].1.is_empty() + || self.variants[0].1.len() == self.variants[0].2.len()) + { + return no_op_body; + } + + let match_arms = self + .variants + .iter() + .filter_map(|(variant, all_fields, skipped_fields)| { + let variant = variant.map(|variant| quote! { :: #variant }); + let self_pattern = all_fields + .non_exhaustive_arm_pattern("__self_", skipped_fields); + + let mut hash_exprs = (0..all_fields.len()) + .filter(|num| !skipped_fields.contains(num)) + .map(|num| { + let self_val = format_ident!("__self_{num}"); + punctuated::Pair::Punctuated(quote! { derive_more::core::hash::Hash::hash(#self_val, state) }, quote!(;)) + }) + .collect::>(); + _ = hash_exprs.pop_punct(); + Some(quote! { + (Self #variant #self_pattern) => { #hash_exprs }, + }) + }) + .collect::>(); + + let discriminant_exprs = self.is_enum.then( || quote!( + let __self_discr = derive_more::core::mem::discriminant(self); + derive_more::core::hash::Hash::hash(&__self_discr, state); + )); + + + let match_expr = (!match_arms.is_empty()).then(|| { + let no_fields_arm = (match_arms.len() != self.variants.len() + || self.has_skipped_variants) + .then(|| { + quote! { _ => #no_op_body } + }); + + quote! { + match (self) { + #( #match_arms )* + #no_fields_arm + } + } + }); + + quote! { + #discriminant_exprs + #match_expr + } + } +} + +impl ToTokens for StructuralExpansion<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ty = self.self_ty.0; + let (_, ty_generics, _) = self.self_ty.1.split_for_impl(); + + let generics_search = GenericsSearch::from(self.self_ty.1); + let mut generics = self.self_ty.1.clone(); + { + let self_ty: syn::Type = parse_quote! { Self }; + let implementor_ty: syn::Type = parse_quote! { #ty #ty_generics }; + for (_, all_fields, skipped_fields) in &self.variants { + for field_ty in + all_fields.iter().enumerate().filter_map(|(n, field)| { + (!skipped_fields.contains(&n)).then_some(&field.ty) + }) + { + if generics_search.any_in(field_ty) + && !field_ty.contains_type_structurally(&self_ty) + && !field_ty.contains_type_structurally(&implementor_ty) + { + generics.make_where_clause().predicates.push(parse_quote! { + #field_ty: derive_more::core::cmp::PartialEq + }); + } + } + } + } + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = self.body(); + let hash_method = + quote! { + #[inline] + fn hash<__H: derive_more::core::hash::Hasher>(&self, state: &mut __H) { #body } + }; + + quote! { + #[allow(private_bounds)] + #[automatically_derived] + impl #impl_generics derive_more::core::hash::Hash for #ty #ty_generics + #where_clause + { + #hash_method + } + } + .to_tokens(tokens); + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index ee23a642..0133ed05 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -28,6 +28,8 @@ mod fmt; mod from; #[cfg(feature = "from_str")] mod from_str; +#[cfg(feature = "hash")] +mod hash; #[cfg(feature = "index")] mod index; #[cfg(feature = "index_mut")] @@ -206,6 +208,8 @@ create_derive!("from", from, From, from_derive, from); create_derive!("from_str", from_str, FromStr, from_str_derive, from_str); +create_derive!("hash", hash, Hash, hash_derive, hash); + create_derive!("index", index, Index, index_derive, index); create_derive!( diff --git a/impl/src/utils.rs b/impl/src/utils.rs index de2d2bf0..e87a860b 100644 --- a/impl/src/utils.rs +++ b/impl/src/utils.rs @@ -21,6 +21,7 @@ use syn::{ feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -36,6 +37,7 @@ pub(crate) use self::fields_ext::FieldsExt; feature = "as_ref", feature = "eq", feature = "from_str", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -47,6 +49,7 @@ pub(crate) use self::generics_search::GenericsSearch; feature = "debug", feature = "display", feature = "eq", + feature = "hash", feature = "from", feature = "from_str", feature = "into", @@ -1327,6 +1330,7 @@ pub fn is_type_parameter_used_in_type( feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1405,6 +1409,7 @@ mod either { feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1507,6 +1512,7 @@ mod spanning { feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1548,6 +1554,7 @@ pub(crate) mod attr { feature = "debug", feature = "eq", feature = "from", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1888,6 +1895,7 @@ pub(crate) mod attr { feature = "display", feature = "eq", feature = "from", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -2515,6 +2523,7 @@ mod fields_ext { feature = "as_ref", feature = "eq", feature = "from_str", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -2869,6 +2878,7 @@ pub(crate) mod replace_self { feature = "add", feature = "add_assign", feature = "eq", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -3028,6 +3038,7 @@ pub(crate) mod structural_inclusion { feature = "add", feature = "add_assign", feature = "eq", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -3037,12 +3048,12 @@ pub(crate) mod pattern_matching { use proc_macro2::TokenStream; use quote::{format_ident, quote}; - #[cfg(any(feature = "add_assign", feature = "eq", feature = "mul_assign"))] + #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] use crate::utils::HashSet; /// Extension of [`syn::Fields`] for pattern matching code generation. pub(crate) trait FieldsExt { - #[cfg(any(feature = "add_assign", feature = "eq", feature = "mul_assign"))] + #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] /// Generates a pattern for matching these [`syn::Fields`] non-exhaustively (considering the /// provided `skipped_indices`) in an arm of a `match` expression. /// @@ -3062,7 +3073,7 @@ pub(crate) mod pattern_matching { } impl FieldsExt for syn::Fields { - #[cfg(any(feature = "add_assign", feature = "eq", feature = "mul_assign"))] + #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] fn non_exhaustive_arm_pattern( &self, prefix: &str, diff --git a/src/lib.rs b/src/lib.rs index 2ffd9a62..4ba38f9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ //! [`MulAssign`-like]: macro@crate::MulAssign //! [`Eq`]: macro@crate::Eq //! [`PartialEq`]: macro@crate::PartialEq +//! [`Hash`]: macro@crate::Hash //! //! [`Constructor`]: macro@crate::Constructor //! [`IsVariant`]: macro@crate::IsVariant @@ -285,6 +286,9 @@ pub mod with_trait { #[cfg(feature = "from_str")] pub use derive_more_impl::FromStr; + #[cfg(feature = "hash")] + pub use derive_more_impl::Hash; + #[cfg(feature = "index")] pub use derive_more_impl::Index; @@ -459,6 +463,7 @@ pub mod with_trait { feature = "error", feature = "from", feature = "from_str", + feature = "hash", feature = "index", feature = "index_mut", feature = "into", diff --git a/tests/hash.rs b/tests/hash.rs new file mode 100644 index 00000000..6a4de572 --- /dev/null +++ b/tests/hash.rs @@ -0,0 +1,166 @@ +fn do_hash(t: &T) -> u64 { + use std::hash::{DefaultHasher, Hasher}; + let mut hasher = DefaultHasher::default(); + t.hash(&mut hasher); + hasher.finish() +} + +mod structs { + mod single_field { + use derive_more::Hash; + use crate::do_hash; + + #[derive(Hash)] + struct Tuple(i32); + + #[derive(Hash)] + struct Struct { + field: i32, + } + + #[derive(Hash)] + struct StructSkipped { + #[hash(skip)] + _skipped: i32, + } + + #[test] + fn assert() { + assert_eq!(do_hash(&Tuple(42)), do_hash(&42)); + assert_eq!(do_hash(&Struct { field: 42 }), do_hash(&42)); + assert_eq!(do_hash(&StructSkipped { _skipped: 42 }), do_hash(&())); + } + } + + mod multi_field { + use derive_more::Hash; + use crate::do_hash; + + #[derive(Hash)] + struct MultiTuple(i32, String, bool); + + #[derive(Hash)] + struct MultiStruct { + a: i32, + b: String, + c: bool, + } + + #[derive(Hash)] + struct MixedSkip { + field1: i32, + #[hash(skip)] + _skipped: String, + field2: bool, + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&MultiTuple(42, "test".to_string(), true)), + do_hash(&(42, "test".to_string(), true)) + ); + assert_eq!( + do_hash(&MultiStruct { + a: 42, + b: "test".to_string(), + c: true + }), + do_hash(&(42, "test".to_string(), true)) + ); + assert_eq!( + do_hash(&MixedSkip { + field1: 42, + _skipped: "ignored".to_string(), + field2: true + }), + do_hash(&(42, true)) + ); + } + } +} + +mod enums { + use derive_more::Hash; + use crate::do_hash; + + #[derive(Hash)] + enum SimpleEnum { + A, + B, + C, + } + + #[derive(Hash)] + enum TupleEnum { + A(i32), + B(String, bool), + C, + } + + #[derive(Hash)] + enum StructEnum { + A { x: i32 }, + B { y: String, z: bool }, + C, + } + + #[derive(Hash)] + enum WithSkip { + A { + field: i32, + #[hash(skip)] + _skipped: String, + }, + B(i32, #[hash(skip)] #[allow(unused)] String), + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&SimpleEnum::A), + do_hash(&std::mem::discriminant(&SimpleEnum::A)) + ); + assert_eq!( + do_hash(&SimpleEnum::B), + do_hash(&std::mem::discriminant(&SimpleEnum::B)) + ); + assert_eq!( + do_hash(&SimpleEnum::C), + do_hash(&std::mem::discriminant(&SimpleEnum::C)) + ); + let ta = TupleEnum::A(42); + let tb = TupleEnum::B("test".to_string(), true); + assert_eq!(do_hash(&ta), do_hash(&(std::mem::discriminant(&ta), 42))); + assert_eq!( + do_hash(&tb), + do_hash(&(std::mem::discriminant(&tb), "test".to_string(), true)) + ); + let tc = TupleEnum::C; + assert_eq!(do_hash(&tc), do_hash(&std::mem::discriminant(&tc))); + + let sa = StructEnum::A { x: 42 }; + assert_eq!(do_hash(&sa), do_hash(&(std::mem::discriminant(&sa), 42))); + + let sb = StructEnum::B { + y: "test".to_string(), + z: true, + }; + assert_eq!( + do_hash(&sb), + do_hash(&(std::mem::discriminant(&sb), "test".to_string(), true)) + ); + + let sc = StructEnum::C; + assert_eq!(do_hash(&sc), do_hash(&std::mem::discriminant(&sc))); + + let wa = WithSkip::A { + field: 42, + _skipped: "ignored".to_string(), + }; + assert_eq!(do_hash(&wa), do_hash(&(std::mem::discriminant(&wa), 42))); + + let wb = WithSkip::B(42, "ignored".to_string()); + assert_eq!(do_hash(&wb), do_hash(&(std::mem::discriminant(&wb), 42))); + } +} From 4be4742189a221bb3266b13231f3be5fd5b7ef20 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Wed, 7 Jan 2026 13:22:31 +0100 Subject: [PATCH 03/12] better tests and doc for Hash --- Cargo.toml | 5 + examples/deny_missing_docs.rs | 3 +- impl/doc/hash.md | 215 ++++++++++++++++++ impl/src/hash.rs | 51 ++--- src/lib.rs | 6 + tests/compile_fail/hash/non_hash_field.rs | 6 + tests/compile_fail/hash/non_hash_field.stderr | 12 + tests/compile_fail/hash/union.rs | 7 + tests/compile_fail/hash/union.stderr | 5 + .../hash/unknown_field_attribute.rs | 4 + .../hash/unknown_field_attribute.stderr | 5 + .../hash/unknown_struct_attribute.rs | 5 + .../hash/unknown_struct_attribute.stderr | 5 + .../hash/unknown_variant_attribute.rs | 6 + .../hash/unknown_variant_attribute.stderr | 5 + tests/hash.rs | 62 ++++- tests/hash_and_eq.rs | 54 +++++ 17 files changed, 424 insertions(+), 32 deletions(-) create mode 100644 tests/compile_fail/hash/non_hash_field.rs create mode 100644 tests/compile_fail/hash/non_hash_field.stderr create mode 100644 tests/compile_fail/hash/union.rs create mode 100644 tests/compile_fail/hash/union.stderr create mode 100644 tests/compile_fail/hash/unknown_field_attribute.rs create mode 100644 tests/compile_fail/hash/unknown_field_attribute.stderr create mode 100644 tests/compile_fail/hash/unknown_struct_attribute.rs create mode 100644 tests/compile_fail/hash/unknown_struct_attribute.stderr create mode 100644 tests/compile_fail/hash/unknown_variant_attribute.rs create mode 100644 tests/compile_fail/hash/unknown_variant_attribute.stderr create mode 100644 tests/hash_and_eq.rs diff --git a/Cargo.toml b/Cargo.toml index 5e9c4cc1..dc270040 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,6 +187,11 @@ name = "hash" path = "tests/hash.rs" required-features = ["hash"] +[[test]] +name = "hash_and_eq" +path = "tests/hash_and_eq.rs" +required-features = ["hash", "eq"] + [[test]] name = "index_mut" path = "tests/index_mut.rs" diff --git a/examples/deny_missing_docs.rs b/examples/deny_missing_docs.rs index 66e0bc8e..1bb2f951 100644 --- a/examples/deny_missing_docs.rs +++ b/examples/deny_missing_docs.rs @@ -4,7 +4,7 @@ #![allow(dead_code)] // for illustration purposes use derive_more::{ - Add, AddAssign, Constructor, Deref, DerefMut, Display, From, FromStr, Index, + Add, AddAssign, Constructor, Deref, DerefMut, Display, From, FromStr, Hash, Index, IndexMut, Into, IsVariant, Mul, MulAssign, Not, TryInto, }; @@ -18,6 +18,7 @@ fn main() {} Display, From, FromStr, + Hash, Into, Mul, MulAssign, diff --git a/impl/doc/hash.md b/impl/doc/hash.md index e69de29b..d4152d71 100644 --- a/impl/doc/hash.md +++ b/impl/doc/hash.md @@ -0,0 +1,215 @@ +# Using `#[derive(Hash)]` + +Deriving `Hash` works by hashing values according to their type structure. + +## Structural hashing + +Deriving `Hash` for enums/structs works in a similar way to the one in `std`, +by hashing all the available fields, but, in contrast: +1. Does not overconstrain generic parameters. +2. Allows to ignore fields, whole structs or enum variants via `#[hash(skip)]` attribute. + +### Structs + +For structs all the available fields are hashed. + +```rust +# use std::marker::PhantomData; +# use derive_more::Hash; +# +trait Trait { + type Assoc; +} +impl Trait for T { + type Assoc = u8; +} + +#[derive(Debug, Hash)] +struct Foo { + a: A, + b: PhantomData, + c: C::Assoc, +} + +#[derive(Debug)] +struct NoHash; +``` + +This generates code equivalent to: + +```rust +# use std::marker::PhantomData; +# use derive_more::core::hash::{Hash, Hasher}; +# +# trait Trait { +# type Assoc; +# } +# impl Trait for T { +# type Assoc = u8; +# } +# +# struct Foo { +# a: A, +# b: PhantomData, +# c: C::Assoc, +# } +# +impl Hash for Foo +where + A: Hash, + PhantomData: Hash, // `B: Hash` is generated by `std` instead + C::Assoc: Hash, // `C: Hash` is generated by `std` instead +{ + fn hash(&self, state: &mut H) { + match self { + Self { a: self_0, b: self_1, c: self_2 } => { + self_0.hash(state); + self_1.hash(state); + self_2.hash(state); + } + } + } +} +``` + +### Enums + +For enums the discriminant is hashed first, followed by the fields. + +```rust +# use std::marker::PhantomData; +# use derive_more::Hash; +# +# trait Trait { +# type Assoc; +# } +# impl Trait for T { +# type Assoc = u8; +# } +# +#[derive(Debug, Hash)] +enum Foo { + A(A), + B { b: PhantomData }, + C(C::Assoc), +} +# +# #[derive(Debug)] +# struct NoHash; +``` + +This generates code equivalent to: + +```rust +# use std::marker::PhantomData; +# use derive_more::core::hash::{Hash, Hasher}; +# +# trait Trait { +# type Assoc; +# } +# impl Trait for T { +# type Assoc = u8; +# } +# +# enum Foo { +# A(A), +# B { b: PhantomData }, +# C(C::Assoc), +# } +# +impl Hash for Foo +where + A: Hash, + PhantomData: Hash, // `B: Hash` is generated by `std` instead + C::Assoc: Hash, // `C: Hash` is generated by `std` instead +{ + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::A(self_0) => { self_0.hash(state); } + Self::B { b: self_0 } => { self_0.hash(state); } + Self::C(self_0) => { self_0.hash(state); } + } + } +} +``` + +### Ignoring + +The `#[hash(skip)]` attribute can be used to ignore fields, a whole struct or enum variants in the expansion. + +Note that if you also implement the `Eq` or `PartialEq` traits, fields marked with `#[eq(skip)]` or `#[partial_eq(skip)]` +will be ignored during hashing. This is done so that this property holds: + +```txt +k1 == k2 -> hash(k1) == hash(k2) +``` +That is [expected](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) from `Hash` implementations. + +```rust +# use derive_more::core::hash::Hash; +# +#[derive(Debug)] +struct NoHash; // doesn't implement `Hash` + +#[derive(Debug, Hash)] +struct Foo { + num: i32, + #[hash(skip)] // or #[hash(ignore)] + ignored: f32, +} + +#[derive(Debug, Hash)] +// Makes all fields of this struct being ignored. +#[hash(skip)] // or #[hash(ignore)] +struct Bar(f32, NoHash); + +#[derive(Debug, Hash)] +enum Enum { + Foo(i32, #[hash(skip)] NoHash), + #[hash(skip)] + Bar(NoHash), + Baz, +} +``` + +This generates code equivalent to: + +```rust +# use derive_more::core::hash::{Hash, Hasher}; +# +# struct NoHash; +# +# struct Foo { num: i32, ignored: f32 } +# +impl Hash for Foo { + fn hash(&self, state: &mut H) { + match self { + Self { num: self_0, .. } => { self_0.hash(state); } + } + } +} + +# struct Bar(i32, NoHash); +# +impl Hash for Bar { + fn hash(&self, _state: &mut H) {} +} + +# enum Enum { +# Foo(i32, NoHash), +# Bar(NoHash), +# Baz, +# } +# +impl Hash for Enum { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::Foo(self_0, _) => { self_0.hash(state); } + Self::Bar(_) => {} + Self::Baz => {} + } + } +} +``` \ No newline at end of file diff --git a/impl/src/hash.rs b/impl/src/hash.rs index d21a5064..eda41261 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -20,13 +20,14 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { - for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + for attr_name in &attr_names { if attr::Skip::parse_attrs(&input.attrs, attr_name)?.is_some() { has_skipped_variants = true; break; @@ -35,7 +36,7 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result syn::Result { 'variants: for variant in &data.variants { - for attr_name in [&attr_name, &secondary_attr_name, &tertiary_attr_name] { + for attr_name in &attr_names { if attr::Skip::parse_attrs(&variant.attrs, attr_name)?.is_some() { has_skipped_variants = true; continue 'variants; @@ -55,7 +56,7 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { /// Generates body of the [`Hash::hash()`] method implementation for this /// [`StructuralExpansion`], if it's required. fn body(&self) -> TokenStream { - let no_op_body = quote! { }; + let no_op_body = quote! { }; // Special case: empty enum. if self.is_enum && self.variants.is_empty() && !self.has_skipped_variants { @@ -128,28 +129,26 @@ impl StructuralExpansion<'_> { } let match_arms = self - .variants - .iter() - .filter_map(|(variant, all_fields, skipped_fields)| { - let variant = variant.map(|variant| quote! { :: #variant }); - let self_pattern = all_fields - .non_exhaustive_arm_pattern("__self_", skipped_fields); - - let mut hash_exprs = (0..all_fields.len()) - .filter(|num| !skipped_fields.contains(num)) - .map(|num| { - let self_val = format_ident!("__self_{num}"); - punctuated::Pair::Punctuated(quote! { derive_more::core::hash::Hash::hash(#self_val, state) }, quote!(;)) - }) - .collect::>(); - _ = hash_exprs.pop_punct(); - Some(quote! { - (Self #variant #self_pattern) => { #hash_exprs }, - }) + .variants + .iter() + .filter_map(|(variant, all_fields, skipped_fields)| { + let variant = variant.map(|variant| quote! { :: #variant }); + let self_pattern = all_fields + .non_exhaustive_arm_pattern("__self_", skipped_fields); + + let mut hash_exprs = (0..all_fields.len()) + .filter(|num| !skipped_fields.contains(num)) + .map(|num| { + let self_val = format_ident!("__self_{num}"); + punctuated::Pair::Punctuated(quote! { derive_more::core::hash::Hash::hash(#self_val, state) }, quote!(;)) + }) + .collect::>(); + _ = hash_exprs.pop_punct(); + Some(quote! { (Self #variant #self_pattern) => { #hash_exprs } }) }) .collect::>(); - let discriminant_exprs = self.is_enum.then( || quote!( + let discriminant_exprs = self.is_enum.then(|| quote!( let __self_discr = derive_more::core::mem::discriminant(self); derive_more::core::hash::Hash::hash(&__self_discr, state); )); @@ -159,7 +158,7 @@ impl StructuralExpansion<'_> { let no_fields_arm = (match_arms.len() != self.variants.len() || self.has_skipped_variants) .then(|| { - quote! { _ => #no_op_body } + quote! { _ => {} } }); quote! { @@ -198,7 +197,7 @@ impl ToTokens for StructuralExpansion<'_> { && !field_ty.contains_type_structurally(&implementor_ty) { generics.make_where_clause().predicates.push(parse_quote! { - #field_ty: derive_more::core::cmp::PartialEq + #field_ty: derive_more::core::hash::Hash }); } } diff --git a/src/lib.rs b/src/lib.rs index 4ba38f9f..cd763273 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -209,6 +209,8 @@ pub mod with_trait { re_export_traits!("from_str", from_str_traits, core::str, FromStr); + re_export_traits!("hash", hash_traits, core::hash, Hash); + re_export_traits!("index", index_traits, core::ops, Index); re_export_traits!("index_mut", index_mut_traits, core::ops, IndexMut); @@ -388,6 +390,10 @@ pub mod with_trait { #[doc(hidden)] pub use all_traits_and_derives::FromStr; + #[cfg(feature = "hash")] + #[doc(hidden)] + pub use all_traits_and_derives::Hash; + #[cfg(feature = "index")] #[doc(hidden)] pub use all_traits_and_derives::Index; diff --git a/tests/compile_fail/hash/non_hash_field.rs b/tests/compile_fail/hash/non_hash_field.rs new file mode 100644 index 00000000..74d4bb94 --- /dev/null +++ b/tests/compile_fail/hash/non_hash_field.rs @@ -0,0 +1,6 @@ +struct NonHashable(i32); + +#[derive(derive_more::Hash)] +struct CantHash(NonHashable); + +fn main() {} \ No newline at end of file diff --git a/tests/compile_fail/hash/non_hash_field.stderr b/tests/compile_fail/hash/non_hash_field.stderr new file mode 100644 index 00000000..26486e88 --- /dev/null +++ b/tests/compile_fail/hash/non_hash_field.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `NonHashable: Hash` is not satisfied + --> tests/compile_fail/hash/non_hash_field.rs:3:10 + | +3 | #[derive(derive_more::Hash)] + | ^^^^^^^^^^^^^^^^^ the trait `Hash` is not implemented for `NonHashable` + | + = note: this error originates in the derive macro `derive_more::Hash` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `NonHashable` with `#[derive(Hash)]` + | +1 + #[derive(Hash)] +2 | struct NonHashable(i32); + | diff --git a/tests/compile_fail/hash/union.rs b/tests/compile_fail/hash/union.rs new file mode 100644 index 00000000..51a16745 --- /dev/null +++ b/tests/compile_fail/hash/union.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Hash)] +union IntOrFloat { + i:u32, + f:f32, +} + +fn main() {} \ No newline at end of file diff --git a/tests/compile_fail/hash/union.stderr b/tests/compile_fail/hash/union.stderr new file mode 100644 index 00000000..4dcc6f67 --- /dev/null +++ b/tests/compile_fail/hash/union.stderr @@ -0,0 +1,5 @@ +error: `Hash` cannot be derived for unions + --> tests/compile_fail/hash/union.rs:2:1 + | +2 | union IntOrFloat { + | ^^^^^ diff --git a/tests/compile_fail/hash/unknown_field_attribute.rs b/tests/compile_fail/hash/unknown_field_attribute.rs new file mode 100644 index 00000000..17bd117e --- /dev/null +++ b/tests/compile_fail/hash/unknown_field_attribute.rs @@ -0,0 +1,4 @@ +#[derive(derive_more::Hash)] +struct Foo(#[hash(unknown)] i32); + +fn main() {} \ No newline at end of file diff --git a/tests/compile_fail/hash/unknown_field_attribute.stderr b/tests/compile_fail/hash/unknown_field_attribute.stderr new file mode 100644 index 00000000..ef306084 --- /dev/null +++ b/tests/compile_fail/hash/unknown_field_attribute.stderr @@ -0,0 +1,5 @@ +error: only `skip`/`ignore` allowed here + --> tests/compile_fail/hash/unknown_field_attribute.rs:2:19 + | +2 | struct Foo(#[hash(unknown)] i32); + | ^^^^^^^ diff --git a/tests/compile_fail/hash/unknown_struct_attribute.rs b/tests/compile_fail/hash/unknown_struct_attribute.rs new file mode 100644 index 00000000..2cd84205 --- /dev/null +++ b/tests/compile_fail/hash/unknown_struct_attribute.rs @@ -0,0 +1,5 @@ +#[derive(derive_more::Hash)] +#[hash(unknown)] +struct Foo(i32); + +fn main() {} \ No newline at end of file diff --git a/tests/compile_fail/hash/unknown_struct_attribute.stderr b/tests/compile_fail/hash/unknown_struct_attribute.stderr new file mode 100644 index 00000000..ac136d2f --- /dev/null +++ b/tests/compile_fail/hash/unknown_struct_attribute.stderr @@ -0,0 +1,5 @@ +error: only `skip`/`ignore` allowed here + --> tests/compile_fail/hash/unknown_struct_attribute.rs:2:8 + | +2 | #[hash(unknown)] + | ^^^^^^^ diff --git a/tests/compile_fail/hash/unknown_variant_attribute.rs b/tests/compile_fail/hash/unknown_variant_attribute.rs new file mode 100644 index 00000000..009d50dc --- /dev/null +++ b/tests/compile_fail/hash/unknown_variant_attribute.rs @@ -0,0 +1,6 @@ +#[derive(derive_more::Hash)] +enum MyEnum { + #[hash(unknown)] + A, +} +fn main() {} \ No newline at end of file diff --git a/tests/compile_fail/hash/unknown_variant_attribute.stderr b/tests/compile_fail/hash/unknown_variant_attribute.stderr new file mode 100644 index 00000000..e1b55a77 --- /dev/null +++ b/tests/compile_fail/hash/unknown_variant_attribute.stderr @@ -0,0 +1,5 @@ +error: only `skip`/`ignore` allowed here + --> tests/compile_fail/hash/unknown_variant_attribute.rs:3:12 + | +3 | #[hash(unknown)] + | ^^^^^^^ diff --git a/tests/hash.rs b/tests/hash.rs index 6a4de572..1554ecfc 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(feature = "std"), no_std)] + fn do_hash(t: &T) -> u64 { use std::hash::{DefaultHasher, Hasher}; let mut hasher = DefaultHasher::default(); @@ -7,8 +9,8 @@ fn do_hash(t: &T) -> u64 { mod structs { mod single_field { - use derive_more::Hash; use crate::do_hash; + use derive_more::Hash; #[derive(Hash)] struct Tuple(i32); @@ -24,17 +26,24 @@ mod structs { _skipped: i32, } + #[derive(Hash)] + #[hash(skip)] + struct StructFullySkipped { + _skipped: i32, + } + #[test] fn assert() { assert_eq!(do_hash(&Tuple(42)), do_hash(&42)); assert_eq!(do_hash(&Struct { field: 42 }), do_hash(&42)); assert_eq!(do_hash(&StructSkipped { _skipped: 42 }), do_hash(&())); + assert_eq!(do_hash(&StructFullySkipped { _skipped: 42 }), do_hash(&())); } } mod multi_field { - use derive_more::Hash; use crate::do_hash; + use derive_more::Hash; #[derive(Hash)] struct MultiTuple(i32, String, bool); @@ -78,11 +87,42 @@ mod structs { ); } } + + mod generics { + use crate::do_hash; + use derive_more::Hash; + + trait SomeTraitWithTypes { + type TraitType; + } + + #[derive(Hash)] + struct GenericStruct { + a: T::TraitType, + b: U, + } + + // this struct doesn't implement `Hash` but implements `SomeTraitWithTypes` with `TraitType = i32` + // this means that `GenericStruct` should be hashable as well + struct SomeTraitWithTypesImpl; + impl SomeTraitWithTypes for SomeTraitWithTypesImpl { + type TraitType = i32; + } + + #[test] + fn assert() { + let instance: GenericStruct = GenericStruct { + a: 42, + b: "test".to_string(), + }; + assert_eq!(do_hash(&instance), do_hash(&(42, "test".to_string()))); + } + } } mod enums { - use derive_more::Hash; use crate::do_hash; + use derive_more::Hash; #[derive(Hash)] enum SimpleEnum { @@ -112,7 +152,15 @@ mod enums { #[hash(skip)] _skipped: String, }, - B(i32, #[hash(skip)] #[allow(unused)] String), + B( + i32, + #[hash(skip)] + #[allow(unused)] + String, + ), + #[hash(skip)] + #[allow(unused)] + C(i32), } #[test] @@ -162,5 +210,9 @@ mod enums { let wb = WithSkip::B(42, "ignored".to_string()); assert_eq!(do_hash(&wb), do_hash(&(std::mem::discriminant(&wb), 42))); + + let wc = WithSkip::C(42); + assert_eq!(do_hash(&wc), do_hash(&std::mem::discriminant(&wc))); + } -} +} \ No newline at end of file diff --git a/tests/hash_and_eq.rs b/tests/hash_and_eq.rs new file mode 100644 index 00000000..650c12e8 --- /dev/null +++ b/tests/hash_and_eq.rs @@ -0,0 +1,54 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use std::hash::{DefaultHasher, Hash, Hasher}; + +fn do_hash(t: &T) -> u64 { + let mut hasher = DefaultHasher::default(); + t.hash(&mut hasher); + hasher.finish() +} + +mod hash_respects_eq_skip { + use super::*; + use derive_more::{Eq, Hash, PartialEq}; + + #[derive(Hash, Eq, PartialEq)] + struct Struct { + field: i32, + #[eq(skip)] + _skipped: String, + } + + #[derive(Hash, Eq, PartialEq)] + enum Enum { + A { + field: i32, + #[partial_eq(skip)] + _skipped: String, + }, + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&Struct { + field: 42, + _skipped: "ignored".to_string() + }), + do_hash(&42) + ); + assert_eq!( + do_hash(&Enum::A { + field: 42, + _skipped: "ignored".to_string() + }), + do_hash(&( + std::mem::discriminant(&Enum::A { + field: 0, + _skipped: String::new() + }), + 42 + )) + ); + } +} From bedc307cc389c13357c98057480f1a43b32ebdc6 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Wed, 7 Jan 2026 18:08:05 +0100 Subject: [PATCH 04/12] add #[hash(with(func)] attribute to Hash --- impl/doc/hash.md | 56 ++++- impl/src/hash.rs | 202 +++++++++++++----- impl/src/utils.rs | 29 +++ .../hash/unknown_field_attribute.stderr | 2 +- tests/hash.rs | 40 +++- 5 files changed, 271 insertions(+), 58 deletions(-) diff --git a/impl/doc/hash.md b/impl/doc/hash.md index d4152d71..c6716bb0 100644 --- a/impl/doc/hash.md +++ b/impl/doc/hash.md @@ -8,6 +8,7 @@ Deriving `Hash` for enums/structs works in a similar way to the one in `std`, by hashing all the available fields, but, in contrast: 1. Does not overconstrain generic parameters. 2. Allows to ignore fields, whole structs or enum variants via `#[hash(skip)]` attribute. +3. Allows to use a custom hash function for a field via `#[hash(with(function))]` attribute. ### Structs @@ -212,4 +213,57 @@ impl Hash for Enum { } } } -``` \ No newline at end of file +``` + +### Custom hash function + +The `#[hash(with(function))]` attribute can be used to specify a custom hash function for a field. +The function must have the signature `fn(value: &FieldType, state: &mut H)`. + +```rust +# use derive_more::Hash; +# +mod my_hash { + use core::hash::Hasher; + + pub fn hash_abs(value: &i32, state: &mut H) { + state.write_i32(value.abs()); + } +} + +#[derive(Hash)] +struct Foo { + #[hash(with(my_hash::hash_abs))] + value: i32, +} +``` + +This generates code equivalent to: + +```rust +# use derive_more::core::hash::{Hash, Hasher}; +# +# mod my_hash { +# use derive_more::core::hash::Hasher; +# +# pub fn hash_abs(value: &i32, state: &mut H) { +# state.write_i32(value.abs()); +# } +# } +# +# struct Foo { +# value: i32, +# } +# +impl Hash for Foo { + fn hash(&self, state: &mut H) { + match self { + Self { value: self_0 } => { + my_hash::hash_abs(self_0, state); + } + } + } +} +``` + +This is useful for types that don't implement `Hash` but can be hashed in a custom way, or when you need different hashing behavior than the default. \ No newline at end of file diff --git a/impl/src/hash.rs b/impl/src/hash.rs index eda41261..a9801a40 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -1,26 +1,59 @@ //! Implementation of an [`Hash`] derive macro. +use crate::utils::{ + attr::{self, ParseMultiple}, + pattern_matching::FieldsExt as _, + structural_inclusion::TypeExt as _, + GenericsSearch, HashMap, HashSet, Spanning, +}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; use syn::{ parse_quote, punctuated::{self, Punctuated}, spanned::Spanned as _, }; -use crate::utils::{ - attr::{self, ParseMultiple as _}, - pattern_matching::FieldsExt as _, - structural_inclusion::TypeExt as _, - GenericsSearch, HashSet, -}; +enum FieldAttributes { + Skip, + With(attr::With), +} + +impl Parse for FieldAttributes { + fn parse(input: ParseStream<'_>) -> syn::Result { + mod ident { + use syn::custom_keyword; + + custom_keyword!(with); + custom_keyword!(skip); + custom_keyword!(ignore); + } + + // We use `.lookahead1()` with all possible idents to form a nice error message including + // all the possible variants. + let ahead = input.lookahead1(); + + if ahead.peek(ident::with) { + Ok(Self::With(input.parse()?)) + } else if ahead.peek(ident::skip) || ahead.peek(ident::ignore) { + let _: attr::Skip = input.parse()?; + Ok(Self::Skip) + } else { + Err(ahead.error()) + } + } +} + +impl ParseMultiple for FieldAttributes {} /// Expands a [`Hash`] derive macro. pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { let attr_name = format_ident!("hash"); let secondary_attr_name = format_ident!("eq"); let tertiary_attr_name = format_ident!("partial_eq"); - let attr_names = [attr_name, secondary_attr_name, tertiary_attr_name]; + let attr_names = [&attr_name, &secondary_attr_name, &tertiary_attr_name]; + let secondary_attr_names = [&secondary_attr_name, &tertiary_attr_name]; let mut has_skipped_variants = false; let mut variants = vec![]; @@ -35,15 +68,41 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { + for attr_name in &secondary_attr_names { + if attr::Skip::parse_attrs(&field.attrs, attr_name)? + .is_some() + { + _ = skipped_fields.insert(n); + continue 'fields; + } + } + } + Some(Spanning { + item: FieldAttributes::Skip, + .. + }) => { + skipped_fields.insert(n); + } + + Some(Spanning { + item: FieldAttributes::With(with), + .. + }) => { + alternate_hash_functions.insert(n, with.path.clone()); } } } - variants.push((None, &data.fields, skipped_fields)); + variants.push(( + None, + &data.fields, + skipped_fields, + alternate_hash_functions, + )); } } syn::Data::Enum(data) => { @@ -55,15 +114,41 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { + for attr_name in &secondary_attr_names { + if attr::Skip::parse_attrs(&field.attrs, attr_name)? + .is_some() + { + _ = skipped_fields.insert(n); + continue 'fields; + } + } + } + Some(Spanning { + item: FieldAttributes::Skip, + .. + }) => { + skipped_fields.insert(n); + } + + Some(Spanning { + item: FieldAttributes::With(with), + .. + }) => { + alternate_hash_functions.insert(n, with.path.clone()); } } } - variants.push((Some(&variant.ident), &variant.fields, skipped_fields)); + variants.push(( + Some(&variant.ident), + &variant.fields, + skipped_fields, + alternate_hash_functions, + )); } } syn::Data::Union(data) => { @@ -80,12 +165,16 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result; +/// Mapping from [`syn::Field`] marked with an [`attr::With`] to the [`syn::Path`] of the alternate +/// hash function. +type FieldsWithAlternateHashFunction = HashMap; + /// Expansion of a macro for generating a structural [`Hash`] implementation of an enum or a /// struct. struct StructuralExpansion<'i> { @@ -95,7 +184,12 @@ struct StructuralExpansion<'i> { self_ty: (&'i syn::Ident, &'i syn::Generics), /// [`syn::Fields`] of the enum/struct to be compared in this [`StructuralExpansion`]. - variants: Vec<(Option<&'i syn::Ident>, &'i syn::Fields, SkippedFields)>, + variants: Vec<( + Option<&'i syn::Ident>, + &'i syn::Fields, + SkippedFields, + FieldsWithAlternateHashFunction, + )>, /// Indicator whether some original enum variants where skipped with an [`attr::Skip`]. has_skipped_variants: bool, @@ -108,7 +202,7 @@ impl StructuralExpansion<'_> { /// Generates body of the [`Hash::hash()`] method implementation for this /// [`StructuralExpansion`], if it's required. fn body(&self) -> TokenStream { - let no_op_body = quote! { }; + let no_op_body = quote! {}; // Special case: empty enum. if self.is_enum && self.variants.is_empty() && !self.has_skipped_variants { @@ -123,7 +217,7 @@ impl StructuralExpansion<'_> { if !(self.is_enum && self.has_skipped_variants) && self.variants.len() == 1 && (self.variants[0].1.is_empty() - || self.variants[0].1.len() == self.variants[0].2.len()) + || self.variants[0].1.len() == self.variants[0].2.len()) { return no_op_body; } @@ -131,28 +225,41 @@ impl StructuralExpansion<'_> { let match_arms = self .variants .iter() - .filter_map(|(variant, all_fields, skipped_fields)| { - let variant = variant.map(|variant| quote! { :: #variant }); - let self_pattern = all_fields - .non_exhaustive_arm_pattern("__self_", skipped_fields); - - let mut hash_exprs = (0..all_fields.len()) - .filter(|num| !skipped_fields.contains(num)) - .map(|num| { - let self_val = format_ident!("__self_{num}"); - punctuated::Pair::Punctuated(quote! { derive_more::core::hash::Hash::hash(#self_val, state) }, quote!(;)) - }) - .collect::>(); - _ = hash_exprs.pop_punct(); - Some(quote! { (Self #variant #self_pattern) => { #hash_exprs } }) - }) - .collect::>(); + .filter_map( + |(variant, all_fields, skipped_fields, alternate_hash_functions)| { + let variant = variant.map(|variant| quote! { :: #variant }); + let self_pattern = all_fields + .non_exhaustive_arm_pattern("__self_", skipped_fields); - let discriminant_exprs = self.is_enum.then(|| quote!( - let __self_discr = derive_more::core::mem::discriminant(self); - derive_more::core::hash::Hash::hash(&__self_discr, state); - )); + let mut hash_exprs = (0..all_fields.len()) + .filter(|num| !skipped_fields.contains(num)) + .map(|num| { + let self_val = format_ident!("__self_{num}"); + let hash_function = alternate_hash_functions + .get(&num) + .map(|it| quote! {#it}) + .unwrap_or_else( + || quote! {derive_more::core::hash::Hash::hash}, + ); + punctuated::Pair::Punctuated( + quote! { #hash_function(#self_val, state) }, + quote!(;), + ) + }) + .collect::>(); + _ = hash_exprs.pop_punct(); + Some(quote! { (Self #variant #self_pattern) => { #hash_exprs } }) + }, + ) + .collect::>(); + + let discriminant_exprs = self.is_enum.then(|| { + quote!( + let __self_discr = derive_more::core::mem::discriminant(self); + derive_more::core::hash::Hash::hash(&__self_discr, state); + ) + }); let match_expr = (!match_arms.is_empty()).then(|| { let no_fields_arm = (match_arms.len() != self.variants.len() @@ -186,7 +293,7 @@ impl ToTokens for StructuralExpansion<'_> { { let self_ty: syn::Type = parse_quote! { Self }; let implementor_ty: syn::Type = parse_quote! { #ty #ty_generics }; - for (_, all_fields, skipped_fields) in &self.variants { + for (_, all_fields, skipped_fields, _) in &self.variants { for field_ty in all_fields.iter().enumerate().filter_map(|(n, field)| { (!skipped_fields.contains(&n)).then_some(&field.ty) @@ -206,11 +313,10 @@ impl ToTokens for StructuralExpansion<'_> { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let body = self.body(); - let hash_method = - quote! { - #[inline] - fn hash<__H: derive_more::core::hash::Hasher>(&self, state: &mut __H) { #body } - }; + let hash_method = quote! { + #[inline] + fn hash<__H: derive_more::core::hash::Hasher>(&self, state: &mut __H) { #body } + }; quote! { #[allow(private_bounds)] @@ -221,6 +327,6 @@ impl ToTokens for StructuralExpansion<'_> { #hash_method } } - .to_tokens(tokens); + .to_tokens(tokens); } } diff --git a/impl/src/utils.rs b/impl/src/utils.rs index e87a860b..34c84a69 100644 --- a/impl/src/utils.rs +++ b/impl/src/utils.rs @@ -1566,6 +1566,9 @@ pub(crate) mod attr { pub(crate) use self::{conversion::Conversion, field_conversion::FieldConversion}; #[cfg(feature = "try_from")] pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt}; + + #[cfg(feature = "hash")] + pub(crate) use self::with::With; /// [`Parse`]ing with additional state or metadata. pub(crate) trait Parser { @@ -2397,6 +2400,32 @@ pub(crate) mod attr { impl ParseMultiple for RenameAll {} } + + #[cfg(feature = "hash")] + mod with { + use syn::parenthesized; + use syn::parse::{Parse, ParseStream}; + use crate::utils::attr::ParseMultiple; + + pub struct With { + pub path: syn::Path, + } + + impl Parse for With { + fn parse(input: ParseStream<'_>) -> syn::Result { + let with = input.parse::()?; + if with != "with" { + return Err(syn::Error::new(with.span(), "unknown attribute argument, expected `with` argument here")); + } + let path_and_parents; + parenthesized!(path_and_parents in input); + let path = path_and_parents.parse::()?; + Ok(Self {path}) + } + } + + impl ParseMultiple for With {} + } } #[cfg(any(feature = "from", feature = "into"))] diff --git a/tests/compile_fail/hash/unknown_field_attribute.stderr b/tests/compile_fail/hash/unknown_field_attribute.stderr index ef306084..8ca7c54c 100644 --- a/tests/compile_fail/hash/unknown_field_attribute.stderr +++ b/tests/compile_fail/hash/unknown_field_attribute.stderr @@ -1,4 +1,4 @@ -error: only `skip`/`ignore` allowed here +error: expected one of: `with`, `skip`, `ignore` --> tests/compile_fail/hash/unknown_field_attribute.rs:2:19 | 2 | struct Foo(#[hash(unknown)] i32); diff --git a/tests/hash.rs b/tests/hash.rs index 1554ecfc..e7f7c3af 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -1,11 +1,17 @@ #![cfg_attr(not(feature = "std"), no_std)] -fn do_hash(t: &T) -> u64 { +fn do_hash(t: &T) -> u64 { use std::hash::{DefaultHasher, Hasher}; let mut hasher = DefaultHasher::default(); t.hash(&mut hasher); hasher.finish() } +mod utils { + pub fn alternate_u32_hash_function(value: &u32, state: &mut H) { + state.write_u32(42); + state.write_u32(*value) + } +} mod structs { mod single_field { @@ -35,7 +41,7 @@ mod structs { #[test] fn assert() { assert_eq!(do_hash(&Tuple(42)), do_hash(&42)); - assert_eq!(do_hash(&Struct { field: 42 }), do_hash(&42)); + assert_eq!(do_hash(&Struct { field: 42}), do_hash(&42)); assert_eq!(do_hash(&StructSkipped { _skipped: 42 }), do_hash(&())); assert_eq!(do_hash(&StructFullySkipped { _skipped: 42 }), do_hash(&())); } @@ -55,6 +61,15 @@ mod structs { c: bool, } + #[derive(Hash)] + struct StructWithAlternateHashFunction { + #[hash(with(crate::utils::alternate_u32_hash_function))] + a: u32, + b: String, + c: bool, + } + + #[derive(Hash)] struct MixedSkip { field1: i32, @@ -77,6 +92,14 @@ mod structs { }), do_hash(&(42, "test".to_string(), true)) ); + assert_eq!( + do_hash(&StructWithAlternateHashFunction { + a:42, + b: "test".to_string(), + c: true + }), + do_hash(&(42, 42, "test".to_string(), true)) + ); assert_eq!( do_hash(&MixedSkip { field1: 42, @@ -146,14 +169,15 @@ mod enums { } #[derive(Hash)] - enum WithSkip { + enum WithAndSkip { A { field: i32, #[hash(skip)] _skipped: String, }, B( - i32, + #[hash(with(crate::utils::alternate_u32_hash_function))] + u32, #[hash(skip)] #[allow(unused)] String, @@ -202,16 +226,16 @@ mod enums { let sc = StructEnum::C; assert_eq!(do_hash(&sc), do_hash(&std::mem::discriminant(&sc))); - let wa = WithSkip::A { + let wa = WithAndSkip::A { field: 42, _skipped: "ignored".to_string(), }; assert_eq!(do_hash(&wa), do_hash(&(std::mem::discriminant(&wa), 42))); - let wb = WithSkip::B(42, "ignored".to_string()); - assert_eq!(do_hash(&wb), do_hash(&(std::mem::discriminant(&wb), 42))); + let wb = WithAndSkip::B(42, "ignored".to_string()); + assert_eq!(do_hash(&wb), do_hash(&(std::mem::discriminant(&wb), 42, 42))); - let wc = WithSkip::C(42); + let wc = WithAndSkip::C(42); assert_eq!(do_hash(&wc), do_hash(&std::mem::discriminant(&wc))); } From 0bc53ba0bb519e71aa617b5804e9b484974fcf17 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Thu, 8 Jan 2026 10:40:50 +0100 Subject: [PATCH 05/12] add hash to changelog and readme --- CHANGELOG.md | 6 ++++++ README.md | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 225d2578..5385b42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + - Add `Hash` derive similar to `std`'s one, but considering generics correctly, + and supporting custom hash functions per field or skipping fields. + ([#532](https://github.com/JelteF/derive_more/pull/532)) + + ## 2.1.1 - 2025-12-22 ### Fixed diff --git a/README.md b/README.md index 0bd97d97..8b82cb4a 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ These are traits that can be used for operator overloading. `ShrAssign` and `ShlAssign` 11. [`Eq`], [`PartialEq`] +### Other traits +1. [`Hash`] ### Static methods @@ -264,6 +266,8 @@ Changing [MSRV] (minimum supported Rust version) of this crate is treated as a * [`Eq`]: https://docs.rs/derive_more/latest/derive_more/derive.Eq.html [`PartialEq`]: https://docs.rs/derive_more/latest/derive_more/derive.PartialEq.html +[`Hash`]: https://docs.rs/derive_more/latest/derive_more/derive.Hash.html + [`Constructor`]: https://docs.rs/derive_more/latest/derive_more/derive.Constructor.html [`IsVariant`]: https://docs.rs/derive_more/latest/derive_more/derive.IsVariant.html [`Unwrap`]: https://docs.rs/derive_more/latest/derive_more/derive.Unwrap.html From 71be6cf630dd47a479e5067c76e6040259cbe1c9 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Thu, 8 Jan 2026 10:41:01 +0100 Subject: [PATCH 06/12] fix ci --- impl/doc/hash.md | 5 +- impl/src/hash.rs | 12 ++--- impl/src/utils.rs | 36 +++++++++++---- tests/hash.rs | 107 +++++++++++++++++++++---------------------- tests/hash_and_eq.rs | 21 ++++----- tests/hash_utils.rs | 30 ++++++++++++ 6 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 tests/hash_utils.rs diff --git a/impl/doc/hash.md b/impl/doc/hash.md index c6716bb0..dc59d578 100644 --- a/impl/doc/hash.md +++ b/impl/doc/hash.md @@ -148,7 +148,7 @@ k1 == k2 -> hash(k1) == hash(k2) That is [expected](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) from `Hash` implementations. ```rust -# use derive_more::core::hash::Hash; +# use derive_more::Hash; # #[derive(Debug)] struct NoHash; // doesn't implement `Hash` @@ -178,7 +178,6 @@ This generates code equivalent to: ```rust # use derive_more::core::hash::{Hash, Hasher}; -# # struct NoHash; # # struct Foo { num: i32, ignored: f32 } @@ -222,7 +221,6 @@ The function must have the signature `fn(value: &FieldType, state: &m ```rust # use derive_more::Hash; -# mod my_hash { use core::hash::Hasher; @@ -242,7 +240,6 @@ This generates code equivalent to: ```rust # use derive_more::core::hash::{Hash, Hasher}; -# # mod my_hash { # use derive_more::core::hash::Hasher; # diff --git a/impl/src/hash.rs b/impl/src/hash.rs index a9801a40..9d9e9960 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -199,7 +199,7 @@ struct StructuralExpansion<'i> { } impl StructuralExpansion<'_> { - /// Generates body of the [`Hash::hash()`] method implementation for this + /// Generates body of the [`core::hash::Hash::hash()`] method implementation for this /// [`StructuralExpansion`], if it's required. fn body(&self) -> TokenStream { let no_op_body = quote! {}; @@ -225,7 +225,7 @@ impl StructuralExpansion<'_> { let match_arms = self .variants .iter() - .filter_map( + .map( |(variant, all_fields, skipped_fields, alternate_hash_functions)| { let variant = variant.map(|variant| quote! { :: #variant }); let self_pattern = all_fields @@ -244,21 +244,21 @@ impl StructuralExpansion<'_> { punctuated::Pair::Punctuated( quote! { #hash_function(#self_val, state) }, - quote!(;), + quote! {;}, ) }) .collect::>(); _ = hash_exprs.pop_punct(); - Some(quote! { (Self #variant #self_pattern) => { #hash_exprs } }) + quote! { (Self #variant #self_pattern) => { #hash_exprs } } }, ) .collect::>(); let discriminant_exprs = self.is_enum.then(|| { - quote!( + quote! { let __self_discr = derive_more::core::mem::discriminant(self); derive_more::core::hash::Hash::hash(&__self_discr, state); - ) + } }); let match_expr = (!match_arms.is_empty()).then(|| { diff --git a/impl/src/utils.rs b/impl/src/utils.rs index 34c84a69..2e8ee994 100644 --- a/impl/src/utils.rs +++ b/impl/src/utils.rs @@ -1566,7 +1566,7 @@ pub(crate) mod attr { pub(crate) use self::{conversion::Conversion, field_conversion::FieldConversion}; #[cfg(feature = "try_from")] pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt}; - + #[cfg(feature = "hash")] pub(crate) use self::with::With; @@ -2403,27 +2403,30 @@ pub(crate) mod attr { #[cfg(feature = "hash")] mod with { + use crate::utils::attr::ParseMultiple; use syn::parenthesized; use syn::parse::{Parse, ParseStream}; - use crate::utils::attr::ParseMultiple; pub struct With { pub path: syn::Path, } - + impl Parse for With { fn parse(input: ParseStream<'_>) -> syn::Result { let with = input.parse::()?; if with != "with" { - return Err(syn::Error::new(with.span(), "unknown attribute argument, expected `with` argument here")); + return Err(syn::Error::new( + with.span(), + "unknown attribute argument, expected `with` argument here", + )); } let path_and_parents; parenthesized!(path_and_parents in input); let path = path_and_parents.parse::()?; - Ok(Self {path}) + Ok(Self { path }) } } - + impl ParseMultiple for With {} } } @@ -3077,12 +3080,22 @@ pub(crate) mod pattern_matching { use proc_macro2::TokenStream; use quote::{format_ident, quote}; - #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] + #[cfg(any( + feature = "add_assign", + feature = "eq", + feature = "hash", + feature = "mul_assign" + ))] use crate::utils::HashSet; /// Extension of [`syn::Fields`] for pattern matching code generation. pub(crate) trait FieldsExt { - #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] + #[cfg(any( + feature = "add_assign", + feature = "eq", + feature = "hash", + feature = "mul_assign" + ))] /// Generates a pattern for matching these [`syn::Fields`] non-exhaustively (considering the /// provided `skipped_indices`) in an arm of a `match` expression. /// @@ -3102,7 +3115,12 @@ pub(crate) mod pattern_matching { } impl FieldsExt for syn::Fields { - #[cfg(any(feature = "add_assign", feature = "eq", feature = "hash", feature = "mul_assign"))] + #[cfg(any( + feature = "add_assign", + feature = "eq", + feature = "hash", + feature = "mul_assign" + ))] fn non_exhaustive_arm_pattern( &self, prefix: &str, diff --git a/tests/hash.rs b/tests/hash.rs index e7f7c3af..8136ffd1 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -1,13 +1,13 @@ #![cfg_attr(not(feature = "std"), no_std)] -fn do_hash(t: &T) -> u64 { - use std::hash::{DefaultHasher, Hasher}; - let mut hasher = DefaultHasher::default(); - t.hash(&mut hasher); - hasher.finish() -} -mod utils { - pub fn alternate_u32_hash_function(value: &u32, state: &mut H) { +mod hash_utils; +use hash_utils::do_hash; + +pub mod utils { + pub fn alternate_u32_hash_function( + value: &u32, + state: &mut H, + ) { state.write_u32(42); state.write_u32(*value) } @@ -41,69 +41,69 @@ mod structs { #[test] fn assert() { assert_eq!(do_hash(&Tuple(42)), do_hash(&42)); - assert_eq!(do_hash(&Struct { field: 42}), do_hash(&42)); + assert_eq!(do_hash(&Struct { field: 42 }), do_hash(&42)); assert_eq!(do_hash(&StructSkipped { _skipped: 42 }), do_hash(&())); assert_eq!(do_hash(&StructFullySkipped { _skipped: 42 }), do_hash(&())); } } mod multi_field { + use super::super::utils; use crate::do_hash; use derive_more::Hash; #[derive(Hash)] - struct MultiTuple(i32, String, bool); + struct MultiTuple(i32, &'static str, bool); #[derive(Hash)] struct MultiStruct { a: i32, - b: String, + b: &'static str, c: bool, } #[derive(Hash)] struct StructWithAlternateHashFunction { - #[hash(with(crate::utils::alternate_u32_hash_function))] + #[hash(with(utils::alternate_u32_hash_function))] a: u32, - b: String, + b: &'static str, c: bool, } - #[derive(Hash)] struct MixedSkip { field1: i32, #[hash(skip)] - _skipped: String, + _skipped: &'static str, field2: bool, } #[test] fn assert() { assert_eq!( - do_hash(&MultiTuple(42, "test".to_string(), true)), - do_hash(&(42, "test".to_string(), true)) + do_hash(&MultiTuple(42, "test", true)), + do_hash(&(42, "test", true)) ); assert_eq!( do_hash(&MultiStruct { a: 42, - b: "test".to_string(), + b: "test", c: true }), - do_hash(&(42, "test".to_string(), true)) + do_hash(&(42, "test", true)) ); assert_eq!( do_hash(&StructWithAlternateHashFunction { - a:42, - b: "test".to_string(), + a: 42, + b: "test", c: true }), - do_hash(&(42, 42, "test".to_string(), true)) + do_hash(&(42, 42, "test", true)) ); assert_eq!( do_hash(&MixedSkip { field1: 42, - _skipped: "ignored".to_string(), + _skipped: "ignored", field2: true }), do_hash(&(42, true)) @@ -134,16 +134,15 @@ mod structs { #[test] fn assert() { - let instance: GenericStruct = GenericStruct { - a: 42, - b: "test".to_string(), - }; - assert_eq!(do_hash(&instance), do_hash(&(42, "test".to_string()))); + let instance: GenericStruct = + GenericStruct { a: 42, b: "test" }; + assert_eq!(do_hash(&instance), do_hash(&(42, "test"))); } } } mod enums { + use super::utils; use crate::do_hash; use derive_more::Hash; @@ -157,14 +156,14 @@ mod enums { #[derive(Hash)] enum TupleEnum { A(i32), - B(String, bool), + B(&'static str, bool), C, } #[derive(Hash)] enum StructEnum { A { x: i32 }, - B { y: String, z: bool }, + B { y: &'static str, z: bool }, C, } @@ -173,14 +172,13 @@ mod enums { A { field: i32, #[hash(skip)] - _skipped: String, + _skipped: &'static str, }, B( - #[hash(with(crate::utils::alternate_u32_hash_function))] - u32, + #[hash(with(utils::alternate_u32_hash_function))] u32, #[hash(skip)] #[allow(unused)] - String, + &'static str, ), #[hash(skip)] #[allow(unused)] @@ -191,52 +189,51 @@ mod enums { fn assert() { assert_eq!( do_hash(&SimpleEnum::A), - do_hash(&std::mem::discriminant(&SimpleEnum::A)) + do_hash(&core::mem::discriminant(&SimpleEnum::A)) ); assert_eq!( do_hash(&SimpleEnum::B), - do_hash(&std::mem::discriminant(&SimpleEnum::B)) + do_hash(&core::mem::discriminant(&SimpleEnum::B)) ); assert_eq!( do_hash(&SimpleEnum::C), - do_hash(&std::mem::discriminant(&SimpleEnum::C)) + do_hash(&core::mem::discriminant(&SimpleEnum::C)) ); let ta = TupleEnum::A(42); - let tb = TupleEnum::B("test".to_string(), true); - assert_eq!(do_hash(&ta), do_hash(&(std::mem::discriminant(&ta), 42))); + let tb = TupleEnum::B("test", true); + assert_eq!(do_hash(&ta), do_hash(&(core::mem::discriminant(&ta), 42))); assert_eq!( do_hash(&tb), - do_hash(&(std::mem::discriminant(&tb), "test".to_string(), true)) + do_hash(&(core::mem::discriminant(&tb), "test", true)) ); let tc = TupleEnum::C; - assert_eq!(do_hash(&tc), do_hash(&std::mem::discriminant(&tc))); + assert_eq!(do_hash(&tc), do_hash(&core::mem::discriminant(&tc))); let sa = StructEnum::A { x: 42 }; - assert_eq!(do_hash(&sa), do_hash(&(std::mem::discriminant(&sa), 42))); + assert_eq!(do_hash(&sa), do_hash(&(core::mem::discriminant(&sa), 42))); - let sb = StructEnum::B { - y: "test".to_string(), - z: true, - }; + let sb = StructEnum::B { y: "test", z: true }; assert_eq!( do_hash(&sb), - do_hash(&(std::mem::discriminant(&sb), "test".to_string(), true)) + do_hash(&(core::mem::discriminant(&sb), "test", true)) ); let sc = StructEnum::C; - assert_eq!(do_hash(&sc), do_hash(&std::mem::discriminant(&sc))); + assert_eq!(do_hash(&sc), do_hash(&core::mem::discriminant(&sc))); let wa = WithAndSkip::A { field: 42, - _skipped: "ignored".to_string(), + _skipped: "ignored", }; - assert_eq!(do_hash(&wa), do_hash(&(std::mem::discriminant(&wa), 42))); + assert_eq!(do_hash(&wa), do_hash(&(core::mem::discriminant(&wa), 42))); - let wb = WithAndSkip::B(42, "ignored".to_string()); - assert_eq!(do_hash(&wb), do_hash(&(std::mem::discriminant(&wb), 42, 42))); + let wb = WithAndSkip::B(42, "ignored"); + assert_eq!( + do_hash(&wb), + do_hash(&(core::mem::discriminant(&wb), 42, 42)) + ); let wc = WithAndSkip::C(42); - assert_eq!(do_hash(&wc), do_hash(&std::mem::discriminant(&wc))); - + assert_eq!(do_hash(&wc), do_hash(&core::mem::discriminant(&wc))); } -} \ No newline at end of file +} diff --git a/tests/hash_and_eq.rs b/tests/hash_and_eq.rs index 650c12e8..f0e2c8e2 100644 --- a/tests/hash_and_eq.rs +++ b/tests/hash_and_eq.rs @@ -1,12 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use std::hash::{DefaultHasher, Hash, Hasher}; - -fn do_hash(t: &T) -> u64 { - let mut hasher = DefaultHasher::default(); - t.hash(&mut hasher); - hasher.finish() -} +mod hash_utils; +use hash_utils::do_hash; mod hash_respects_eq_skip { use super::*; @@ -16,7 +11,7 @@ mod hash_respects_eq_skip { struct Struct { field: i32, #[eq(skip)] - _skipped: String, + _skipped: &'static str, } #[derive(Hash, Eq, PartialEq)] @@ -24,7 +19,7 @@ mod hash_respects_eq_skip { A { field: i32, #[partial_eq(skip)] - _skipped: String, + _skipped: &'static str, }, } @@ -33,19 +28,19 @@ mod hash_respects_eq_skip { assert_eq!( do_hash(&Struct { field: 42, - _skipped: "ignored".to_string() + _skipped: "ignored" }), do_hash(&42) ); assert_eq!( do_hash(&Enum::A { field: 42, - _skipped: "ignored".to_string() + _skipped: "ignored" }), do_hash(&( - std::mem::discriminant(&Enum::A { + core::mem::discriminant(&Enum::A { field: 0, - _skipped: String::new() + _skipped: "ignored" }), 42 )) diff --git a/tests/hash_utils.rs b/tests/hash_utils.rs new file mode 100644 index 00000000..aaa30322 --- /dev/null +++ b/tests/hash_utils.rs @@ -0,0 +1,30 @@ +#[cfg(feature = "std")] +pub fn do_hash(t: &T) -> u64 { + use std::hash::{DefaultHasher, Hasher}; + let mut hasher = DefaultHasher::default(); + t.hash(&mut hasher); + hasher.finish() +} + +#[cfg(not(feature = "std"))] +pub fn do_hash(t: &T) -> u64 { + use core::hash::Hasher; + // Simple FNV-1a hasher for no_std, for testing purposes only. + struct FnvHasher(u64); + + impl core::hash::Hasher for FnvHasher { + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.0 ^= *byte as u64; + self.0 = self.0.wrapping_mul(0x100000001b3); + } + } + fn finish(&self) -> u64 { + self.0 + } + } + + let mut hasher = FnvHasher(0xcbf29ce484222325); + t.hash(&mut hasher); + hasher.finish() +} From ef8aad5a38388051c9e0f970ec5d240ca4f02484 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Thu, 8 Jan 2026 15:08:44 +0100 Subject: [PATCH 07/12] typo --- impl/src/hash.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/src/hash.rs b/impl/src/hash.rs index 9d9e9960..7ed2db31 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -183,7 +183,7 @@ struct StructuralExpansion<'i> { /// [`syn::Ident`]: struct@syn::Ident self_ty: (&'i syn::Ident, &'i syn::Generics), - /// [`syn::Fields`] of the enum/struct to be compared in this [`StructuralExpansion`]. + /// [`syn::Fields`] of the enum/struct to be hashed in this [`StructuralExpansion`]. variants: Vec<( Option<&'i syn::Ident>, &'i syn::Fields, From aecafc31b996c05a29f6f750d3463b24981f3107 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Fri, 9 Jan 2026 11:31:21 +0100 Subject: [PATCH 08/12] add missing section in changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5385b42e..6f402235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + +### Added - Add `Hash` derive similar to `std`'s one, but considering generics correctly, and supporting custom hash functions per field or skipping fields. ([#532](https://github.com/JelteF/derive_more/pull/532)) From 832dfba449fc37182499ce49c2fcaf9a0aa3d1c3 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Tue, 27 Jan 2026 10:02:28 +0100 Subject: [PATCH 09/12] move all hash tests to single file --- Cargo.toml | 5 --- tests/hash.rs | 78 ++++++++++++++++++++++++++++++++++++++++++-- tests/hash_and_eq.rs | 49 ---------------------------- tests/hash_utils.rs | 30 ----------------- 4 files changed, 76 insertions(+), 86 deletions(-) delete mode 100644 tests/hash_and_eq.rs delete mode 100644 tests/hash_utils.rs diff --git a/Cargo.toml b/Cargo.toml index dc270040..5e9c4cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,11 +187,6 @@ name = "hash" path = "tests/hash.rs" required-features = ["hash"] -[[test]] -name = "hash_and_eq" -path = "tests/hash_and_eq.rs" -required-features = ["hash", "eq"] - [[test]] name = "index_mut" path = "tests/index_mut.rs" diff --git a/tests/hash.rs b/tests/hash.rs index 8136ffd1..d6b96d11 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -1,7 +1,35 @@ #![cfg_attr(not(feature = "std"), no_std)] -mod hash_utils; -use hash_utils::do_hash; +#[cfg(feature = "std")] +pub fn do_hash(t: &T) -> u64 { + use std::hash::{DefaultHasher, Hasher}; + let mut hasher = DefaultHasher::default(); + t.hash(&mut hasher); + hasher.finish() +} + +#[cfg(not(feature = "std"))] +pub fn do_hash(t: &T) -> u64 { + use core::hash::Hasher; + // Simple FNV-1a hasher for no_std, for testing purposes only. + struct FnvHasher(u64); + + impl core::hash::Hasher for FnvHasher { + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.0 ^= *byte as u64; + self.0 = self.0.wrapping_mul(0x100000001b3); + } + } + fn finish(&self) -> u64 { + self.0 + } + } + + let mut hasher = FnvHasher(0xcbf29ce484222325); + t.hash(&mut hasher); + hasher.finish() +} pub mod utils { pub fn alternate_u32_hash_function( @@ -237,3 +265,49 @@ mod enums { assert_eq!(do_hash(&wc), do_hash(&core::mem::discriminant(&wc))); } } + +#[cfg(feature = "eq")] +mod hash_respects_eq_skip { + use super::*; + use derive_more::{Eq, Hash, PartialEq}; + + #[derive(Hash, Eq, PartialEq)] + struct Struct { + field: i32, + #[eq(skip)] + _skipped: &'static str, + } + + #[derive(Hash, Eq, PartialEq)] + enum Enum { + A { + field: i32, + #[partial_eq(skip)] + _skipped: &'static str, + }, + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&Struct { + field: 42, + _skipped: "ignored" + }), + do_hash(&42) + ); + assert_eq!( + do_hash(&Enum::A { + field: 42, + _skipped: "ignored" + }), + do_hash(&( + core::mem::discriminant(&Enum::A { + field: 0, + _skipped: "ignored" + }), + 42 + )) + ); + } +} diff --git a/tests/hash_and_eq.rs b/tests/hash_and_eq.rs deleted file mode 100644 index f0e2c8e2..00000000 --- a/tests/hash_and_eq.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -mod hash_utils; -use hash_utils::do_hash; - -mod hash_respects_eq_skip { - use super::*; - use derive_more::{Eq, Hash, PartialEq}; - - #[derive(Hash, Eq, PartialEq)] - struct Struct { - field: i32, - #[eq(skip)] - _skipped: &'static str, - } - - #[derive(Hash, Eq, PartialEq)] - enum Enum { - A { - field: i32, - #[partial_eq(skip)] - _skipped: &'static str, - }, - } - - #[test] - fn assert() { - assert_eq!( - do_hash(&Struct { - field: 42, - _skipped: "ignored" - }), - do_hash(&42) - ); - assert_eq!( - do_hash(&Enum::A { - field: 42, - _skipped: "ignored" - }), - do_hash(&( - core::mem::discriminant(&Enum::A { - field: 0, - _skipped: "ignored" - }), - 42 - )) - ); - } -} diff --git a/tests/hash_utils.rs b/tests/hash_utils.rs deleted file mode 100644 index aaa30322..00000000 --- a/tests/hash_utils.rs +++ /dev/null @@ -1,30 +0,0 @@ -#[cfg(feature = "std")] -pub fn do_hash(t: &T) -> u64 { - use std::hash::{DefaultHasher, Hasher}; - let mut hasher = DefaultHasher::default(); - t.hash(&mut hasher); - hasher.finish() -} - -#[cfg(not(feature = "std"))] -pub fn do_hash(t: &T) -> u64 { - use core::hash::Hasher; - // Simple FNV-1a hasher for no_std, for testing purposes only. - struct FnvHasher(u64); - - impl core::hash::Hasher for FnvHasher { - fn write(&mut self, bytes: &[u8]) { - for byte in bytes { - self.0 ^= *byte as u64; - self.0 = self.0.wrapping_mul(0x100000001b3); - } - } - fn finish(&self) -> u64 { - self.0 - } - } - - let mut hasher = FnvHasher(0xcbf29ce484222325); - t.hash(&mut hasher); - hasher.finish() -} From 3fd81154c85b78cb85965bf8e0205c9383ce6b66 Mon Sep 17 00:00:00 2001 From: Thibaut Lorrain Date: Tue, 27 Jan 2026 10:27:33 +0100 Subject: [PATCH 10/12] add more tests around possible hash collisions --- tests/hash.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/hash.rs b/tests/hash.rs index d6b96d11..a7c270b9 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -106,6 +106,12 @@ mod structs { field2: bool, } + #[derive(Hash)] + struct PossibleCollision { + a: &'static str, + b: &'static str, + } + #[test] fn assert() { assert_eq!( @@ -136,6 +142,10 @@ mod structs { }), do_hash(&(42, true)) ); + assert_ne!( + do_hash(&PossibleCollision { a: "a", b: "bc" }), + do_hash(&PossibleCollision { a: "ab", b: "c" }) + ); } } @@ -188,6 +198,13 @@ mod enums { C, } + #[derive(Hash)] + enum SameDataEnum { + A(i32), + B(i32), + C(i32), + } + #[derive(Hash)] enum StructEnum { A { x: i32 }, @@ -227,6 +244,12 @@ mod enums { do_hash(&SimpleEnum::C), do_hash(&core::mem::discriminant(&SimpleEnum::C)) ); + let sa = SameDataEnum::A(42); + assert_eq!(do_hash(&sa), do_hash(&(core::mem::discriminant(&sa), 42))); + let sb = SameDataEnum::B(42); + assert_eq!(do_hash(&sb), do_hash(&(core::mem::discriminant(&sb), 42))); + let sc = SameDataEnum::C(42); + assert_eq!(do_hash(&sc), do_hash(&(core::mem::discriminant(&sc), 42))); let ta = TupleEnum::A(42); let tb = TupleEnum::B("test", true); assert_eq!(do_hash(&ta), do_hash(&(core::mem::discriminant(&ta), 42))); From 64c7d39771d429a8b52095a247f3c4e9dc68701d Mon Sep 17 00:00:00 2001 From: Kai Ren Date: Mon, 4 May 2026 22:11:50 +0300 Subject: [PATCH 11/12] Bikeshed, vol.1 --- .gitignore | 2 +- README.md | 3 ++ impl/src/hash.rs | 75 ++++++++++++++++++++++++----------------------- impl/src/utils.rs | 21 ++++++++----- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index baf66e28..1772761f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,5 @@ gh-pages .swp /tags -# RustRover/Other jetbrains IDE files +# RustRover/Other JetBrains IDE files .idea/ diff --git a/README.md b/README.md index 8b82cb4a..2d2a2601 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,12 @@ These are traits that can be used for operator overloading. `ShrAssign` and `ShlAssign` 11. [`Eq`], [`PartialEq`] + ### Other traits + 1. [`Hash`] + ### Static methods These don't derive traits, but derive static methods instead. diff --git a/impl/src/hash.rs b/impl/src/hash.rs index 7ed2db31..e02d97a1 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -1,4 +1,4 @@ -//! Implementation of an [`Hash`] derive macro. +//! Implementation of a [`Hash`] derive macro. use crate::utils::{ attr::{self, ParseMultiple}, @@ -15,38 +15,6 @@ use syn::{ spanned::Spanned as _, }; -enum FieldAttributes { - Skip, - With(attr::With), -} - -impl Parse for FieldAttributes { - fn parse(input: ParseStream<'_>) -> syn::Result { - mod ident { - use syn::custom_keyword; - - custom_keyword!(with); - custom_keyword!(skip); - custom_keyword!(ignore); - } - - // We use `.lookahead1()` with all possible idents to form a nice error message including - // all the possible variants. - let ahead = input.lookahead1(); - - if ahead.peek(ident::with) { - Ok(Self::With(input.parse()?)) - } else if ahead.peek(ident::skip) || ahead.peek(ident::ignore) { - let _: attr::Skip = input.parse()?; - Ok(Self::Skip) - } else { - Err(ahead.error()) - } - } -} - -impl ParseMultiple for FieldAttributes {} - /// Expands a [`Hash`] derive macro. pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { let attr_name = format_ident!("hash"); @@ -168,6 +136,38 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result) -> syn::Result { + mod ident { + use syn::custom_keyword; + + custom_keyword!(with); + custom_keyword!(skip); + custom_keyword!(ignore); + } + + // We use `.lookahead1()` with all possible idents to form a nice error message including + // all the possible variants. + let ahead = input.lookahead1(); + + if ahead.peek(ident::with) { + Ok(Self::With(input.parse()?)) + } else if ahead.peek(ident::skip) || ahead.peek(ident::ignore) { + let _: attr::Skip = input.parse()?; + Ok(Self::Skip) + } else { + Err(ahead.error()) + } + } +} + +impl ParseMultiple for FieldAttributes {} + /// Indices of [`syn::Field`]s marked with an [`attr::Skip`]. type SkippedFields = HashSet; @@ -175,8 +175,7 @@ type SkippedFields = HashSet; /// hash function. type FieldsWithAlternateHashFunction = HashMap; -/// Expansion of a macro for generating a structural [`Hash`] implementation of an enum or a -/// struct. +/// Expansion of a macro for generating a structural [`Hash`] implementation of an enum or a struct. struct StructuralExpansion<'i> { /// [`syn::Ident`] and [`syn::Generics`] of the enum/struct. /// @@ -199,8 +198,10 @@ struct StructuralExpansion<'i> { } impl StructuralExpansion<'_> { - /// Generates body of the [`core::hash::Hash::hash()`] method implementation for this - /// [`StructuralExpansion`], if it's required. + /// Generates a body of the [`Hash::hash()`] method implementation for this + /// [`StructuralExpansion`]. + /// + /// [`Hash::hash()`]: core::hash::Hash::hash fn body(&self) -> TokenStream { let no_op_body = quote! {}; diff --git a/impl/src/utils.rs b/impl/src/utils.rs index e87b41f6..8cb4db12 100644 --- a/impl/src/utils.rs +++ b/impl/src/utils.rs @@ -1570,7 +1570,6 @@ pub(crate) mod attr { pub(crate) use self::{conversion::Conversion, field_conversion::FieldConversion}; #[cfg(feature = "try_from")] pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt}; - #[cfg(feature = "hash")] pub(crate) use self::with::With; @@ -2407,12 +2406,20 @@ pub(crate) mod attr { #[cfg(feature = "hash")] mod with { - use crate::utils::attr::ParseMultiple; use syn::parenthesized; use syn::parse::{Parse, ParseStream}; - pub struct With { - pub path: syn::Path, + use crate::utils::attr::ParseMultiple; + + /// Representation of an attribute, specifying a custom function for a trait method. + /// + /// ```rust,ignore + /// #[(with())] + /// #[(error(, ))] + /// ``` + pub(crate) struct With { + /// Custom function. + pub(crate) func: syn::Path, // TODO: Support `syn::ExprCall` and `syn::ExprClosure` too. } impl Parse for With { @@ -2421,13 +2428,13 @@ pub(crate) mod attr { if with != "with" { return Err(syn::Error::new( with.span(), - "unknown attribute argument, expected `with` argument here", + "unknown attribute argument, expected `with(...)` argument here", )); } let path_and_parents; parenthesized!(path_and_parents in input); - let path = path_and_parents.parse::()?; - Ok(Self { path }) + let func = path_and_parents.parse::()?; + Ok(Self { func }) } } From dc31c689cf59b11fcee5f1757d6a7fbe0afcdae5 Mon Sep 17 00:00:00 2001 From: Kai Ren Date: Sat, 9 May 2026 14:31:15 +0300 Subject: [PATCH 12/12] Bikeshed --- .gitignore | 3 --- impl/doc/hash.md | 16 ++++++++++++---- impl/src/hash.rs | 22 +++++++++++++-------- impl/src/utils.rs | 5 ++--- tests/hash.rs | 49 ++++++++++++++++++++++++++++------------------- 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 1772761f..59b51f17 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,3 @@ gh-pages # git files .swp /tags - -# RustRover/Other JetBrains IDE files -.idea/ diff --git a/impl/doc/hash.md b/impl/doc/hash.md index dc59d578..b19731a2 100644 --- a/impl/doc/hash.md +++ b/impl/doc/hash.md @@ -2,6 +2,9 @@ Deriving `Hash` works by hashing values according to their type structure. + + + ## Structural hashing Deriving `Hash` for enums/structs works in a similar way to the one in `std`, @@ -10,6 +13,7 @@ by hashing all the available fields, but, in contrast: 2. Allows to ignore fields, whole structs or enum variants via `#[hash(skip)]` attribute. 3. Allows to use a custom hash function for a field via `#[hash(with(function))]` attribute. + ### Structs For structs all the available fields are hashed. @@ -135,17 +139,19 @@ where } ``` + ### Ignoring The `#[hash(skip)]` attribute can be used to ignore fields, a whole struct or enum variants in the expansion. -Note that if you also implement the `Eq` or `PartialEq` traits, fields marked with `#[eq(skip)]` or `#[partial_eq(skip)]` -will be ignored during hashing. This is done so that this property holds: +Note, that if you also implement the `Eq` or `PartialEq` traits, fields marked with +`#[eq(skip)]` or `#[partial_eq(skip)]` will be ignored during hashing. This is done so that this property holds: ```txt k1 == k2 -> hash(k1) == hash(k2) ``` -That is [expected](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) from `Hash` implementations. +That is the [expected](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) property from `Hash` +implementations. ```rust # use derive_more::Hash; @@ -214,6 +220,7 @@ impl Hash for Enum { } ``` + ### Custom hash function The `#[hash(with(function))]` attribute can be used to specify a custom hash function for a field. @@ -263,4 +270,5 @@ impl Hash for Foo { } ``` -This is useful for types that don't implement `Hash` but can be hashed in a custom way, or when you need different hashing behavior than the default. \ No newline at end of file +This is useful for types that don't implement `Hash` but can be hashed in a custom way, or when you need different +hashing behavior than the default. diff --git a/impl/src/hash.rs b/impl/src/hash.rs index e02d97a1..c78ae7b3 100644 --- a/impl/src/hash.rs +++ b/impl/src/hash.rs @@ -61,7 +61,7 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { - alternate_hash_functions.insert(n, with.path.clone()); + alternate_hash_functions.insert(n, with.func.clone()); } } } @@ -107,7 +107,7 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result { - alternate_hash_functions.insert(n, with.path.clone()); + alternate_hash_functions.insert(n, with.func.clone()); } } } @@ -136,11 +136,17 @@ pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result) -> syn::Result { mod ident { @@ -151,14 +157,14 @@ impl Parse for FieldAttributes { custom_keyword!(ignore); } - // We use `.lookahead1()` with all possible idents to form a nice error message including - // all the possible variants. + // `.lookahead1()` with all possible idents forms a nice error message including all the + // possible variants. let ahead = input.lookahead1(); if ahead.peek(ident::with) { Ok(Self::With(input.parse()?)) } else if ahead.peek(ident::skip) || ahead.peek(ident::ignore) { - let _: attr::Skip = input.parse()?; + _ = input.parse::()?; Ok(Self::Skip) } else { Err(ahead.error()) @@ -198,9 +204,9 @@ struct StructuralExpansion<'i> { } impl StructuralExpansion<'_> { - /// Generates a body of the [`Hash::hash()`] method implementation for this + /// Generates a body of the [`Hash::hash()`] method implementation for this /// [`StructuralExpansion`]. - /// + /// /// [`Hash::hash()`]: core::hash::Hash::hash fn body(&self) -> TokenStream { let no_op_body = quote! {}; @@ -271,7 +277,7 @@ impl StructuralExpansion<'_> { quote! { match (self) { - #( #match_arms )* + #( #match_arms )* #no_fields_arm } } diff --git a/impl/src/utils.rs b/impl/src/utils.rs index 8cb4db12..82178bb1 100644 --- a/impl/src/utils.rs +++ b/impl/src/utils.rs @@ -1566,12 +1566,12 @@ pub(crate) mod attr { pub(crate) use self::skip::Skip; #[cfg(any(feature = "as_ref", feature = "from", feature = "try_from"))] pub(crate) use self::types::Types; + #[cfg(feature = "hash")] + pub(crate) use self::with::With; #[cfg(any(feature = "as_ref", feature = "from"))] pub(crate) use self::{conversion::Conversion, field_conversion::FieldConversion}; #[cfg(feature = "try_from")] pub(crate) use self::{repr_conversion::ReprConversion, repr_int::ReprInt}; - #[cfg(feature = "hash")] - pub(crate) use self::with::With; /// [`Parse`]ing with additional state or metadata. pub(crate) trait Parser { @@ -2415,7 +2415,6 @@ pub(crate) mod attr { /// /// ```rust,ignore /// #[(with())] - /// #[(error(, ))] /// ``` pub(crate) struct With { /// Custom function. diff --git a/tests/hash.rs b/tests/hash.rs index a7c270b9..f9854280 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -43,9 +43,10 @@ pub mod utils { mod structs { mod single_field { - use crate::do_hash; use derive_more::Hash; + use crate::do_hash; + #[derive(Hash)] struct Tuple(i32); @@ -76,10 +77,10 @@ mod structs { } mod multi_field { - use super::super::utils; - use crate::do_hash; use derive_more::Hash; + use crate::{do_hash, utils}; + #[derive(Hash)] struct MultiTuple(i32, &'static str, bool); @@ -150,9 +151,10 @@ mod structs { } mod generics { - use crate::do_hash; use derive_more::Hash; + use crate::do_hash; + trait SomeTraitWithTypes { type TraitType; } @@ -163,8 +165,9 @@ mod structs { b: U, } - // this struct doesn't implement `Hash` but implements `SomeTraitWithTypes` with `TraitType = i32` - // this means that `GenericStruct` should be hashable as well + // this struct doesn't implement `Hash` but implements `SomeTraitWithTypes` with + // `TraitType = i32` this means that `GenericStruct` should be + // hashable as well struct SomeTraitWithTypesImpl; impl SomeTraitWithTypes for SomeTraitWithTypesImpl { type TraitType = i32; @@ -180,10 +183,10 @@ mod structs { } mod enums { - use super::utils; - use crate::do_hash; use derive_more::Hash; + use crate::{do_hash, utils}; + #[derive(Hash)] enum SimpleEnum { A, @@ -234,29 +237,34 @@ mod enums { fn assert() { assert_eq!( do_hash(&SimpleEnum::A), - do_hash(&core::mem::discriminant(&SimpleEnum::A)) + do_hash(&core::mem::discriminant(&SimpleEnum::A)), ); assert_eq!( do_hash(&SimpleEnum::B), - do_hash(&core::mem::discriminant(&SimpleEnum::B)) + do_hash(&core::mem::discriminant(&SimpleEnum::B)), ); assert_eq!( do_hash(&SimpleEnum::C), - do_hash(&core::mem::discriminant(&SimpleEnum::C)) + do_hash(&core::mem::discriminant(&SimpleEnum::C)), ); + let sa = SameDataEnum::A(42); assert_eq!(do_hash(&sa), do_hash(&(core::mem::discriminant(&sa), 42))); + let sb = SameDataEnum::B(42); assert_eq!(do_hash(&sb), do_hash(&(core::mem::discriminant(&sb), 42))); + let sc = SameDataEnum::C(42); assert_eq!(do_hash(&sc), do_hash(&(core::mem::discriminant(&sc), 42))); + let ta = TupleEnum::A(42); let tb = TupleEnum::B("test", true); assert_eq!(do_hash(&ta), do_hash(&(core::mem::discriminant(&ta), 42))); assert_eq!( do_hash(&tb), - do_hash(&(core::mem::discriminant(&tb), "test", true)) + do_hash(&(core::mem::discriminant(&tb), "test", true)), ); + let tc = TupleEnum::C; assert_eq!(do_hash(&tc), do_hash(&core::mem::discriminant(&tc))); @@ -266,7 +274,7 @@ mod enums { let sb = StructEnum::B { y: "test", z: true }; assert_eq!( do_hash(&sb), - do_hash(&(core::mem::discriminant(&sb), "test", true)) + do_hash(&(core::mem::discriminant(&sb), "test", true)), ); let sc = StructEnum::C; @@ -281,7 +289,7 @@ mod enums { let wb = WithAndSkip::B(42, "ignored"); assert_eq!( do_hash(&wb), - do_hash(&(core::mem::discriminant(&wb), 42, 42)) + do_hash(&(core::mem::discriminant(&wb), 42, 42)), ); let wc = WithAndSkip::C(42); @@ -291,9 +299,10 @@ mod enums { #[cfg(feature = "eq")] mod hash_respects_eq_skip { - use super::*; use derive_more::{Eq, Hash, PartialEq}; + use super::do_hash; + #[derive(Hash, Eq, PartialEq)] struct Struct { field: i32, @@ -315,21 +324,21 @@ mod hash_respects_eq_skip { assert_eq!( do_hash(&Struct { field: 42, - _skipped: "ignored" + _skipped: "ignored", }), - do_hash(&42) + do_hash(&42), ); assert_eq!( do_hash(&Enum::A { field: 42, - _skipped: "ignored" + _skipped: "ignored", }), do_hash(&( core::mem::discriminant(&Enum::A { field: 0, - _skipped: "ignored" + _skipped: "ignored", }), - 42 + 42, )) ); }