diff --git a/Cargo.lock b/Cargo.lock
index e501321056..e781ee1141 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1672,6 +1672,7 @@ dependencies = [
"pest_derive",
"pollster",
"pretty_assertions",
+ "proc-macros",
"prost",
"rand",
"rand_chacha",
@@ -2213,13 +2214,22 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.76"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
+[[package]]
+name = "proc-macros"
+version = "0.14.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "prodash"
version = "28.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 3f7f41e483..3aabcb7141 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@ cargo-features = []
[workspace]
resolver = "2"
-members = ["cli", "lib", "lib/testutils", "lib/gen-protos"]
+members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"]
[workspace.package]
version = "0.14.0"
@@ -66,8 +66,10 @@ pest = "2.7.7"
pest_derive = "2.7.7"
pollster = "0.3.0"
pretty_assertions = "1.4.0"
+proc-macro2 = "1.0.78"
prost = "0.12.3"
prost-build = "0.12.3"
+quote = "1.0.35"
rand = "0.8.5"
rand_chacha = "0.3.1"
rayon = "1.8.1"
@@ -85,6 +87,7 @@ smallvec = { version = "1.13.0", features = [
"union",
] }
strsim = "0.11.0"
+syn = "2.0.48"
tempfile = "3.10.0"
test-case = "3.3.1"
textwrap = "0.16.0"
@@ -110,6 +113,7 @@ zstd = "0.12.4"
# their own (alphabetically sorted) block
jj-lib = { path = "lib", version = "0.14.0" }
+proc-macros = { path = "lib/proc-macros" }
testutils = { path = "lib/testutils" }
[profile.release]
diff --git a/lib/Cargo.toml b/lib/Cargo.toml
index 7a480cd05e..da26335b3f 100644
--- a/lib/Cargo.toml
+++ b/lib/Cargo.toml
@@ -42,6 +42,7 @@ once_cell = { workspace = true }
pest = { workspace = true }
pest_derive = { workspace = true }
pollster = { workspace = true }
+proc-macros = { workspace = true }
prost = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
diff --git a/lib/proc-macros/Cargo.toml b/lib/proc-macros/Cargo.toml
new file mode 100644
index 0000000000..42e04fdc87
--- /dev/null
+++ b/lib/proc-macros/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "proc-macros"
+publish = false
+
+version = { workspace = true }
+edition = { workspace = true }
+license = { workspace = true }
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = {workspace=true}
+quote ={workspace=true}
+proc-macro2 = {workspace=true}
diff --git a/lib/proc-macros/src/content_hash.rs b/lib/proc-macros/src/content_hash.rs
new file mode 100644
index 0000000000..a487bb64ee
--- /dev/null
+++ b/lib/proc-macros/src/content_hash.rs
@@ -0,0 +1,38 @@
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::spanned::Spanned;
+use syn::{Data, Fields, Index};
+
+pub fn generate_hash_impl(data: &Data) -> TokenStream {
+ match *data {
+ Data::Struct(ref data) => match data.fields {
+ Fields::Named(ref fields) => {
+ let hash_statements = fields.named.iter().map(|f| {
+ let field_name = &f.ident;
+ quote_spanned! {f.span()=>
+ crate::content_hash::ContentHash::hash(&self.#field_name, state);
+ // self.#field_name.hash(state);
+ }
+ });
+ quote! {
+ #(#hash_statements)*
+ }
+ }
+ Fields::Unnamed(ref fields) => {
+ let hash_statements = fields.unnamed.iter().enumerate().map(|(i, f)| {
+ let index = Index::from(i);
+ quote_spanned! {f.span() =>
+ crate::content_hash::ContentHash::hash(&self.#index, state);
+ }
+ });
+ quote! {
+ #(#hash_statements)*
+ }
+ }
+ Fields::Unit => {
+ quote! {}
+ }
+ },
+ _ => unimplemented!("ContentHash can only be derived for structs."),
+ }
+}
diff --git a/lib/proc-macros/src/lib.rs b/lib/proc-macros/src/lib.rs
new file mode 100644
index 0000000000..cdbeae4829
--- /dev/null
+++ b/lib/proc-macros/src/lib.rs
@@ -0,0 +1,29 @@
+mod content_hash;
+
+extern crate proc_macro;
+
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+/// Derives the `ContentHash` trait for a struct by calling `ContentHash::hash`
+/// on each of the struct members in the order that they're declared. All
+/// members of the struct must implement the `ContentHash` trait.
+#[proc_macro_derive(ContentHash)]
+pub fn derive_content_hash(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+
+ // The name of the struct.
+ let name = &input.ident;
+
+ // Generate an expression to hash each of the fields in the struct.
+ let hash_impl = content_hash::generate_hash_impl(&input.data);
+
+ let expanded = quote! {
+ impl crate::content_hash::ContentHash for #name {
+ fn hash(&self, state: &mut impl digest::Update) {
+ #hash_impl
+ }
+ }
+ };
+ expanded.into()
+}
diff --git a/lib/src/content_hash.rs b/lib/src/content_hash.rs
index 8f5d67a3a7..2ec21ff4aa 100644
--- a/lib/src/content_hash.rs
+++ b/lib/src/content_hash.rs
@@ -2,6 +2,7 @@
use blake2::Blake2b512;
use itertools::Itertools as _;
+pub use proc_macros::ContentHash;
/// Portable, stable hashing suitable for identifying values
///
@@ -231,6 +232,31 @@ mod tests {
);
}
+ // This will be removed once all uses of content_hash! are replaced by the
+ // derive version.
+ #[test]
+ fn derive_is_equivalent_to_macro() {
+ content_hash! {
+ struct FooMacro { x: Vec