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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 679e859c..833502c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## master +### 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)) + ### Fixed - Mistakenly generated code for `owned` type in `TryInto`, `Unwrap` and `TryUnwrap` 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/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 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/Cargo.toml b/impl/Cargo.toml index a1441154..cfd82220 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..dc59d578 --- /dev/null +++ b/impl/doc/hash.md @@ -0,0 +1,266 @@ +# 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. +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. + +```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::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 => {} + } + } +} +``` + +### 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 new file mode 100644 index 00000000..7ed2db31 --- /dev/null +++ b/impl/src/hash.rs @@ -0,0 +1,332 @@ +//! 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 _, +}; + +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 secondary_attr_names = [&secondary_attr_name, &tertiary_attr_name]; + + let mut has_skipped_variants = false; + let mut variants = vec![]; + + match &input.data { + syn::Data::Struct(data) => { + for attr_name in &attr_names { + 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(); + let mut alternate_hash_functions = + FieldsWithAlternateHashFunction::default(); + 'fields: for (n, field) in data.fields.iter().enumerate() { + match FieldAttributes::parse_attrs(&field.attrs, &attr_name)? { + None => { + 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, + alternate_hash_functions, + )); + } + } + syn::Data::Enum(data) => { + 'variants: for variant in &data.variants { + for attr_name in &attr_names { + if attr::Skip::parse_attrs(&variant.attrs, attr_name)?.is_some() { + has_skipped_variants = true; + continue 'variants; + } + } + let mut skipped_fields = SkippedFields::default(); + let mut alternate_hash_functions = + FieldsWithAlternateHashFunction::default(); + 'fields: for (n, field) in variant.fields.iter().enumerate() { + match FieldAttributes::parse_attrs(&field.attrs, &attr_name)? { + None => { + 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, + alternate_hash_functions, + )); + } + } + 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; + +/// 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> { + /// [`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 hashed in this [`StructuralExpansion`]. + 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, + + /// Indicator whether this expansion is for an enum. + is_enum: bool, +} + +impl StructuralExpansion<'_> { + /// 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! {}; + + // 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() + .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 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(); + 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! { _ => {} } + }); + + 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::hash::Hash + }); + } + } + } + } + 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 463e7dae..e87b41f6 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", @@ -1331,6 +1334,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", @@ -1409,6 +1413,7 @@ mod either { feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1511,6 +1516,7 @@ mod spanning { feature = "eq", feature = "from", feature = "from_str", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1552,6 +1558,7 @@ pub(crate) mod attr { feature = "debug", feature = "eq", feature = "from", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -1564,6 +1571,9 @@ pub(crate) mod attr { #[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 { /// [`Parse`]s an item, using additional state or metadata. @@ -1892,6 +1902,7 @@ pub(crate) mod attr { feature = "display", feature = "eq", feature = "from", + feature = "hash", feature = "into", feature = "mul", feature = "mul_assign", @@ -2393,6 +2404,35 @@ pub(crate) mod attr { impl ParseMultiple for RenameAll {} } + + #[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, + } + + 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"))] @@ -2519,6 +2559,7 @@ mod fields_ext { feature = "as_ref", feature = "eq", feature = "from_str", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -2873,6 +2914,7 @@ pub(crate) mod replace_self { feature = "add", feature = "add_assign", feature = "eq", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -3032,6 +3074,7 @@ pub(crate) mod structural_inclusion { feature = "add", feature = "add_assign", feature = "eq", + feature = "hash", feature = "mul", feature = "mul_assign", ))] @@ -3041,12 +3084,22 @@ 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. /// @@ -3066,7 +3119,12 @@ 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..cd763273 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 @@ -208,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); @@ -285,6 +288,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; @@ -384,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; @@ -459,6 +469,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/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..8ca7c54c --- /dev/null +++ b/tests/compile_fail/hash/unknown_field_attribute.stderr @@ -0,0 +1,5 @@ +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/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 new file mode 100644 index 00000000..a7c270b9 --- /dev/null +++ b/tests/hash.rs @@ -0,0 +1,336 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[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( + value: &u32, + state: &mut H, + ) { + state.write_u32(42); + state.write_u32(*value) + } +} + +mod structs { + mod single_field { + use crate::do_hash; + use derive_more::Hash; + + #[derive(Hash)] + struct Tuple(i32); + + #[derive(Hash)] + struct Struct { + field: i32, + } + + #[derive(Hash)] + struct StructSkipped { + #[hash(skip)] + _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 super::super::utils; + use crate::do_hash; + use derive_more::Hash; + + #[derive(Hash)] + struct MultiTuple(i32, &'static str, bool); + + #[derive(Hash)] + struct MultiStruct { + a: i32, + b: &'static str, + c: bool, + } + + #[derive(Hash)] + struct StructWithAlternateHashFunction { + #[hash(with(utils::alternate_u32_hash_function))] + a: u32, + b: &'static str, + c: bool, + } + + #[derive(Hash)] + struct MixedSkip { + field1: i32, + #[hash(skip)] + _skipped: &'static str, + field2: bool, + } + + #[derive(Hash)] + struct PossibleCollision { + a: &'static str, + b: &'static str, + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&MultiTuple(42, "test", true)), + do_hash(&(42, "test", true)) + ); + assert_eq!( + do_hash(&MultiStruct { + a: 42, + b: "test", + c: true + }), + do_hash(&(42, "test", true)) + ); + assert_eq!( + do_hash(&StructWithAlternateHashFunction { + a: 42, + b: "test", + c: true + }), + do_hash(&(42, 42, "test", true)) + ); + assert_eq!( + do_hash(&MixedSkip { + field1: 42, + _skipped: "ignored", + field2: true + }), + do_hash(&(42, true)) + ); + assert_ne!( + do_hash(&PossibleCollision { a: "a", b: "bc" }), + do_hash(&PossibleCollision { a: "ab", b: "c" }) + ); + } + } + + 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" }; + assert_eq!(do_hash(&instance), do_hash(&(42, "test"))); + } + } +} + +mod enums { + use super::utils; + use crate::do_hash; + use derive_more::Hash; + + #[derive(Hash)] + enum SimpleEnum { + A, + B, + C, + } + + #[derive(Hash)] + enum TupleEnum { + A(i32), + B(&'static str, bool), + C, + } + + #[derive(Hash)] + enum SameDataEnum { + A(i32), + B(i32), + C(i32), + } + + #[derive(Hash)] + enum StructEnum { + A { x: i32 }, + B { y: &'static str, z: bool }, + C, + } + + #[derive(Hash)] + enum WithAndSkip { + A { + field: i32, + #[hash(skip)] + _skipped: &'static str, + }, + B( + #[hash(with(utils::alternate_u32_hash_function))] u32, + #[hash(skip)] + #[allow(unused)] + &'static str, + ), + #[hash(skip)] + #[allow(unused)] + C(i32), + } + + #[test] + fn assert() { + assert_eq!( + do_hash(&SimpleEnum::A), + do_hash(&core::mem::discriminant(&SimpleEnum::A)) + ); + assert_eq!( + do_hash(&SimpleEnum::B), + do_hash(&core::mem::discriminant(&SimpleEnum::B)) + ); + assert_eq!( + 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))); + assert_eq!( + do_hash(&tb), + do_hash(&(core::mem::discriminant(&tb), "test", true)) + ); + let tc = TupleEnum::C; + assert_eq!(do_hash(&tc), do_hash(&core::mem::discriminant(&tc))); + + let sa = StructEnum::A { x: 42 }; + assert_eq!(do_hash(&sa), do_hash(&(core::mem::discriminant(&sa), 42))); + + let sb = StructEnum::B { y: "test", z: true }; + assert_eq!( + do_hash(&sb), + do_hash(&(core::mem::discriminant(&sb), "test", true)) + ); + + let sc = StructEnum::C; + assert_eq!(do_hash(&sc), do_hash(&core::mem::discriminant(&sc))); + + let wa = WithAndSkip::A { + field: 42, + _skipped: "ignored", + }; + assert_eq!(do_hash(&wa), do_hash(&(core::mem::discriminant(&wa), 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(&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 + )) + ); + } +}