feat(abigen): support shared type during multiabigen (#959)

* refactor: move type deduplication to abigen crate

* refactor: separate expansion process

* feat: support shared type during multiabigen

* test: add type deduplication test

* chore: rustfmt

* typos

* chore(clippy): make clippy happy

* chore: add anonymous field
This commit is contained in:
Matthias Seitz 2022-02-24 21:09:08 +01:00 committed by GitHub
parent 72da5913d2
commit f5ef8149e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 538 additions and 247 deletions

View File

@ -76,7 +76,10 @@ pub struct Context {
human_readable: bool, human_readable: bool,
/// The contract name as an identifier. /// The contract name as an identifier.
contract_name: Ident, contract_ident: Ident,
/// The contract name as string
contract_name: String,
/// Manually specified method aliases. /// Manually specified method aliases.
method_aliases: BTreeMap<String, MethodAlias>, method_aliases: BTreeMap<String, MethodAlias>,
@ -91,9 +94,9 @@ pub struct Context {
impl Context { impl Context {
/// Expands the whole rust contract /// Expands the whole rust contract
pub fn expand(&self) -> Result<ExpandedContract> { pub fn expand(&self) -> Result<ExpandedContract> {
let name = &self.contract_name; let name = &self.contract_ident;
let name_mod = let name_mod =
util::ident(&format!("{}_mod", self.contract_name.to_string().to_lowercase())); util::ident(&format!("{}_mod", self.contract_ident.to_string().to_lowercase()));
let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase())); let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase()));
@ -190,7 +193,7 @@ impl Context {
.unwrap_or_default() .unwrap_or_default()
}; };
let contract_name = util::ident(&args.contract_name); let contract_ident = util::ident(&args.contract_name);
// NOTE: We only check for duplicate signatures here, since if there are // NOTE: We only check for duplicate signatures here, since if there are
// duplicate aliases, the compiler will produce a warning because a // duplicate aliases, the compiler will produce a warning because a
@ -226,13 +229,19 @@ impl Context {
abi_str: Literal::string(&abi_str), abi_str: Literal::string(&abi_str),
abi_parser, abi_parser,
internal_structs, internal_structs,
contract_name, contract_ident,
contract_name: args.contract_name,
method_aliases, method_aliases,
event_derives, event_derives,
event_aliases, event_aliases,
}) })
} }
/// The initial name fo the contract
pub(crate) fn contract_name(&self) -> &str {
&self.contract_name
}
/// The internal abi struct mapping table /// The internal abi struct mapping table
pub fn internal_structs(&self) -> &InternalStructs { pub fn internal_structs(&self) -> &InternalStructs {
&self.internal_structs &self.internal_structs

View File

@ -31,7 +31,7 @@ pub(crate) fn imports(name: &str) -> TokenStream {
/// Generates the static `Abi` constants and the contract struct /// Generates the static `Abi` constants and the contract struct
pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> TokenStream { pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> TokenStream {
let name = &cx.contract_name; let name = &cx.contract_ident;
let abi = &cx.abi_str; let abi = &cx.abi_str;
let ethers_core = ethers_core_crate(); let ethers_core = ethers_core_crate();

View File

@ -102,7 +102,7 @@ impl Context {
/// The name ident of the events enum /// The name ident of the events enum
fn expand_event_enum_name(&self) -> Ident { fn expand_event_enum_name(&self) -> Ident {
util::ident(&format!("{}Events", self.contract_name)) util::ident(&format!("{}Events", self.contract_ident))
} }
/// Expands the `events` function that bundles all declared events of this contract /// Expands the `events` function that bundles all declared events of this contract

View File

@ -161,7 +161,7 @@ impl Context {
/// The name ident of the calls enum /// The name ident of the calls enum
fn expand_calls_enum_name(&self) -> Ident { fn expand_calls_enum_name(&self) -> Ident {
util::ident(&format!("{}Calls", self.contract_name)) util::ident(&format!("{}Calls", self.contract_ident))
} }
/// Expands to the `name : type` pairs of the function's inputs /// Expands to the `name : type` pairs of the function's inputs

View File

@ -26,6 +26,7 @@ pub use ethers_core::types::Address;
pub use source::Source; pub use source::Source;
pub use util::parse_address; pub use util::parse_address;
use crate::contract::ExpandedContract;
use eyre::Result; use eyre::Result;
use inflector::Inflector; use inflector::Inflector;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
@ -87,7 +88,7 @@ impl Abigen {
}) })
} }
/// Attemtps to load a new builder from an ABI JSON file at the specific /// Attempts to load a new builder from an ABI JSON file at the specific
/// path. /// path.
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> { pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let name = path let name = path
@ -153,8 +154,15 @@ impl Abigen {
pub fn generate(self) -> Result<ContractBindings> { pub fn generate(self) -> Result<ContractBindings> {
let rustfmt = self.rustfmt; let rustfmt = self.rustfmt;
let name = self.contract_name.clone(); let name = self.contract_name.clone();
let tokens = Context::from_abigen(self)?.expand()?.into_tokens(); let (expanded, _) = self.expand()?;
Ok(ContractBindings { tokens, rustfmt, name }) Ok(ContractBindings { tokens: expanded.into_tokens(), rustfmt, name })
}
/// Expands the `Abigen` and returns the [`ExpandedContract`] that holds all tokens and the
/// [`Context`] that holds the state used during expansion.
pub fn expand(self) -> Result<(ExpandedContract, Context)> {
let ctx = Context::from_abigen(self)?;
Ok((ctx.expand()?, ctx))
} }
} }

View File

@ -2,9 +2,190 @@
use eyre::Result; use eyre::Result;
use inflector::Inflector; use inflector::Inflector;
use std::{collections::BTreeMap, fs, io::Write, path::Path}; use proc_macro2::TokenStream;
use quote::quote;
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fs,
io::Write,
path::Path,
};
use crate::{util, Abigen, ContractBindings}; use crate::{util, Abigen, Context, ContractBindings, ExpandedContract};
/// Represents a collection of [`Abigen::expand()`]
pub struct MultiExpansion {
// all expanded contracts collection from [`Abigen::expand()`]
contracts: Vec<(ExpandedContract, Context)>,
}
impl MultiExpansion {
/// Create a new instance that wraps the given `contracts`
pub fn new(contracts: Vec<(ExpandedContract, Context)>) -> Self {
Self { contracts }
}
/// Create a new instance by expanding all `Abigen` elements the given iterator yields
pub fn from_abigen(abigens: impl IntoIterator<Item = Abigen>) -> Result<Self> {
let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::<Result<_>>()?;
Ok(Self::new(contracts))
}
/// Expands all contracts into a single `TokenStream`
///
/// This will deduplicate types into a separate `mod __shared_types` module, if any.
pub fn expand_inplace(self) -> TokenStream {
self.expand().expand_inplace()
}
/// Expands all contracts into separated [`TokenStream`]s
///
/// If there was type deduplication, this returns a list of [`TokenStream`] containing the type
/// definitions of all shared types.
pub fn expand(self) -> MultiExpansionResult {
let mut expansions = self.contracts;
let mut shared_types = Vec::new();
// this keeps track of those contracts that need to be updated after a struct was
// extracted from the contract's module and moved to the shared module
let mut dirty_contracts = HashSet::new();
// merge all types if more than 1 contract
if expansions.len() > 1 {
// check for type conflicts across all contracts
let mut conflicts: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, (_, ctx)) in expansions.iter().enumerate() {
for type_identifier in ctx.internal_structs().rust_type_names().keys() {
conflicts
.entry(type_identifier.clone())
.or_insert_with(|| Vec::with_capacity(1))
.push(idx);
}
}
// resolve type conflicts
for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
// extract the shared type once
shared_types.push(
expansions[contracts[0]]
.1
.struct_definition(id)
.expect("struct def succeeded previously"),
);
// remove the shared type from the contract's bindings
for contract in contracts.iter().copied() {
expansions[contract].1.remove_struct(id);
dirty_contracts.insert(contract);
}
}
// regenerate all struct definitions that were hit
for contract in dirty_contracts.iter().copied() {
let (expanded, ctx) = &mut expansions[contract];
expanded.abi_structs = ctx.abi_structs().expect("struct def succeeded previously");
}
}
MultiExpansionResult { contracts: expansions, dirty_contracts, shared_types }
}
}
/// Represents an intermediary result of [`MultiExpansion::expand()`]
pub struct MultiExpansionResult {
contracts: Vec<(ExpandedContract, Context)>,
/// contains the indices of contracts with structs that need to be updated
dirty_contracts: HashSet<usize>,
/// all type definitions of types that are shared by multiple contracts
shared_types: Vec<TokenStream>,
}
impl MultiExpansionResult {
/// Expands all contracts into a single [`TokenStream`]
pub fn expand_inplace(mut self) -> TokenStream {
let mut tokens = TokenStream::new();
let shared_types_module = quote! {__shared_types};
// the import path to the shared types
let shared_path = quote!(
pub use super::#shared_types_module::*;
);
self.add_shared_import_path(shared_path);
let Self { contracts, shared_types, .. } = self;
tokens.extend(quote! {
pub mod #shared_types_module {
#( #shared_types )*
}
});
tokens.extend(contracts.into_iter().map(|(exp, _)| exp.into_tokens()));
tokens
}
/// Sets the path to the shared types module according to the value of `single_file`
///
/// If `single_file` then it's expected that types will be written to `shared_types.rs`
fn set_shared_import_path(&mut self, single_file: bool) {
let shared_path = if single_file {
quote!(
pub use super::__shared_types::*;
)
} else {
quote!(
pub use super::super::shared_types::*;
)
};
self.add_shared_import_path(shared_path);
}
/// adds the `shared` import path to every `dirty` contract
fn add_shared_import_path(&mut self, shared: TokenStream) {
for contract in self.dirty_contracts.iter().copied() {
let (expanded, ..) = &mut self.contracts[contract];
expanded.imports.extend(shared.clone());
}
}
/// Converts this result into [`MultiBindingsInner`]
fn into_bindings(mut self, single_file: bool, rustfmt: bool) -> MultiBindingsInner {
self.set_shared_import_path(single_file);
let Self { contracts, shared_types, .. } = self;
let bindings = contracts
.into_iter()
.map(|(expanded, ctx)| ContractBindings {
tokens: expanded.into_tokens(),
rustfmt,
name: ctx.contract_name().to_string(),
})
.map(|v| (v.name.clone(), v))
.collect();
let shared_types = if !shared_types.is_empty() {
let shared_types = if single_file {
quote! {
pub mod __shared_types {
#( #shared_types )*
}
}
} else {
quote! {
#( #shared_types )*
}
};
Some(ContractBindings {
tokens: shared_types,
rustfmt,
name: "shared_types".to_string(),
})
} else {
None
};
MultiBindingsInner { bindings, shared_types }
}
}
/// Collects Abigen structs for a series of contracts, pending generation of /// Collects Abigen structs for a series of contracts, pending generation of
/// the contract bindings. /// the contract bindings.
@ -86,16 +267,11 @@ impl MultiAbigen {
/// Build the contract bindings and prepare for writing /// Build the contract bindings and prepare for writing
pub fn build(self) -> Result<MultiBindings> { pub fn build(self) -> Result<MultiBindings> {
let bindings = self let rustfmt = self.abigens.iter().any(|gen| gen.rustfmt);
.abigens Ok(MultiBindings {
.into_iter() expansion: MultiExpansion::from_abigen(self.abigens)?.expand(),
.map(|v| v.generate()) rustfmt,
.collect::<Result<Vec<_>>>()? })
.into_iter()
.map(|v| (v.name.clone(), v))
.collect();
Ok(MultiBindings { bindings })
} }
} }
@ -126,12 +302,188 @@ impl MultiAbigen {
/// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself /// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
/// changed) /// changed)
pub struct MultiBindings { pub struct MultiBindings {
expansion: MultiExpansionResult,
rustfmt: bool,
}
impl MultiBindings {
fn into_inner(self, single_file: bool) -> MultiBindingsInner {
self.expansion.into_bindings(single_file, self.rustfmt)
}
/// Generates all the bindings and writes them to the given module
///
/// # Example
///
/// Read all json abi files from the `./abi` directory
/// ```text
/// abi
/// ├── ERC20.json
/// ├── Contract1.json
/// ├── Contract2.json
/// ...
/// ```
///
/// and write them to the `./src/contracts` location as
///
/// ```text
/// src/contracts
/// ├── mod.rs
/// ├── er20.rs
/// ├── contract1.rs
/// ├── contract2.rs
/// ...
/// ```
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.write_to_module("./src/contracts", false).unwrap();
/// ```
pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
self.into_inner(single_file).write_to_module(module, single_file)
}
/// Generates all the bindings and writes a library crate containing them
/// to the provided path
///
/// # Example
///
/// Read all json abi files from the `./abi` directory
/// ```text
/// abi
/// ├── ERC20.json
/// ├── Contract1.json
/// ├── Contract2.json
/// ├── Contract3/
/// ├── Contract3.json
/// ...
/// ```
///
/// and write them to the `./bindings` location as
///
/// ```text
/// bindings
/// ├── Cargo.toml
/// ├── src/
/// ├── lib.rs
/// ├── er20.rs
/// ├── contract1.rs
/// ├── contract2.rs
/// ...
/// ```
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.write_to_crate(
/// "my-crate", "0.0.5", "./bindings", false
/// ).unwrap();
/// ```
pub fn write_to_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
lib: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.into_inner(single_file).write_to_crate(name, version, lib, single_file)
}
/// This ensures that the already generated bindings crate matches the
/// output of a fresh new run. Run this in a rust test, to get notified in
/// CI if the newly generated bindings deviate from the already generated
/// ones, and it's time to generate them again. This could happen if the
/// ABI of a contract or the output that `ethers` generates changed.
///
/// If this functions is run within a test during CI and fails, then it's
/// time to update all bindings.
///
/// # Returns
///
/// `Ok(())` if the freshly generated bindings match with the
/// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
/// with more information.
///
/// # Example
///
/// Check that the generated files are up to date
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// #[test]
/// fn generated_bindings_are_fresh() {
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
/// let abi_dir = project_root.join("abi");
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.ensure_consistent_crate(
/// "my-crate", "0.0.1", project_root.join("src/contracts"), false
/// ).expect("inconsistent bindings");
/// }
/// ```
pub fn ensure_consistent_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
crate_path: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.into_inner(single_file).ensure_consistent_crate(name, version, crate_path, single_file)
}
/// This ensures that the already generated bindings module matches the
/// output of a fresh new run. Run this in a rust test, to get notified in
/// CI if the newly generated bindings deviate from the already generated
/// ones, and it's time to generate them again. This could happen if the
/// ABI of a contract or the output that `ethers` generates changed.
///
/// If this functions is run within a test during CI and fails, then it's
/// time to update all bindings.
///
/// # Returns
///
/// `Ok(())` if the freshly generated bindings match with the
/// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
/// with more information.
///
/// # Example
///
/// Check that the generated files are up to date
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// #[test]
/// fn generated_bindings_are_fresh() {
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
/// let abi_dir = project_root.join("abi");
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.ensure_consistent_module(
/// project_root.join("src/contracts"), false
/// ).expect("inconsistent bindings");
/// }
/// ```
pub fn ensure_consistent_module(
self,
module: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.into_inner(single_file).ensure_consistent_module(module, single_file)
}
}
struct MultiBindingsInner {
/// Abigen objects to be written /// Abigen objects to be written
bindings: BTreeMap<String, ContractBindings>, bindings: BTreeMap<String, ContractBindings>,
/// contains the content of the shared types if any
shared_types: Option<ContractBindings>,
} }
// deref allows for inspection without modification // deref allows for inspection without modification
impl std::ops::Deref for MultiBindings { impl std::ops::Deref for MultiBindingsInner {
type Target = BTreeMap<String, ContractBindings>; type Target = BTreeMap<String, ContractBindings>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -139,8 +491,8 @@ impl std::ops::Deref for MultiBindings {
} }
} }
impl MultiBindings { impl MultiBindingsInner {
/// Generat the contents of the `Cargo.toml` file for a lib /// Generate the contents of the `Cargo.toml` file for a lib
fn generate_cargo_toml( fn generate_cargo_toml(
&self, &self,
name: impl AsRef<str>, name: impl AsRef<str>,
@ -185,34 +537,14 @@ serde_json = "1.0.79"
Ok(()) Ok(())
} }
/// Generate the shared prefix of the `lib.rs` or `mod.rs`
fn generate_prefix(
&self,
mut buf: impl Write,
is_crate: bool,
single_file: bool,
) -> Result<()> {
writeln!(buf, "#![allow(clippy::all)]")?;
writeln!(
buf,
"//! This {} contains abigen! generated bindings for solidity contracts.",
if is_crate { "lib" } else { "module" }
)?;
writeln!(buf, "//! This is autogenerated code.")?;
writeln!(buf, "//! Do not manually edit these files.")?;
writeln!(
buf,
"//! {} may be overwritten by the codegen system at any time.",
if single_file && !is_crate { "This file" } else { "These files" }
)?;
Ok(())
}
/// Append module declarations to the `lib.rs` or `mod.rs` /// Append module declarations to the `lib.rs` or `mod.rs`
fn append_module_names(&self, mut buf: impl Write) -> Result<()> { fn append_module_names(&self, mut buf: impl Write) -> Result<()> {
// sorting here not necessary, as btreemap keys are ordered let mut mod_names: BTreeSet<_> = self.bindings.keys().collect();
for module in self.bindings.keys().map(|name| format!("pub mod {};", name.to_snake_case())) if let Some(ref shared) = self.shared_types {
mod_names.insert(&shared.name);
}
for module in mod_names.into_iter().map(|name| format!("pub mod {};", name.to_snake_case()))
{ {
writeln!(buf, "{}", module)?; writeln!(buf, "{}", module)?;
} }
@ -223,14 +555,17 @@ serde_json = "1.0.79"
/// Generate the contents of `lib.rs` or `mod.rs` /// Generate the contents of `lib.rs` or `mod.rs`
fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> { fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> {
let mut contents = vec![]; let mut contents = vec![];
self.generate_prefix(&mut contents, is_crate, single_file)?; generate_prefix(&mut contents, is_crate, single_file)?;
if !single_file { if single_file {
self.append_module_names(&mut contents)?; if let Some(ref shared) = self.shared_types {
} else { shared.write(&mut contents)?;
}
for binding in self.bindings.values() { for binding in self.bindings.values() {
binding.write(&mut contents)?; binding.write(&mut contents)?;
} }
} else {
self.append_module_names(&mut contents)?;
} }
Ok(contents) Ok(contents)
@ -246,43 +581,16 @@ serde_json = "1.0.79"
/// Write all contract bindings to their respective files /// Write all contract bindings to their respective files
fn write_bindings(&self, path: &Path) -> Result<()> { fn write_bindings(&self, path: &Path) -> Result<()> {
if let Some(ref shared) = self.shared_types {
shared.write_module_in_dir(path)?;
}
for binding in self.bindings.values() { for binding in self.bindings.values() {
binding.write_module_in_dir(path)?; binding.write_module_in_dir(path)?;
} }
Ok(()) Ok(())
} }
/// Generates all the bindings and writes them to the given module fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
///
/// # Example
///
/// Read all json abi files from the `./abi` directory
/// ```text
/// abi
/// ├── ERC20.json
/// ├── Contract1.json
/// ├── Contract2.json
/// ...
/// ```
///
/// and write them to the `./src/contracts` location as
///
/// ```text
/// src/contracts
/// ├── mod.rs
/// ├── er20.rs
/// ├── contract1.rs
/// ├── contract2.rs
/// ...
/// ```
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.write_to_module("./src/contracts", false).unwrap();
/// ```
pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
let module = module.as_ref(); let module = module.as_ref();
fs::create_dir_all(module)?; fs::create_dir_all(module)?;
@ -294,44 +602,7 @@ serde_json = "1.0.79"
Ok(()) Ok(())
} }
/// Generates all the bindings and writes a library crate containing them fn write_to_crate(
/// to the provided path
///
/// # Example
///
/// Read all json abi files from the `./abi` directory
/// ```text
/// abi
/// ├── ERC20.json
/// ├── Contract1.json
/// ├── Contract2.json
/// ├── Contract3/
/// ├── Contract3.json
/// ...
/// ```
///
/// and write them to the `./bindings` location as
///
/// ```text
/// bindings
/// ├── Cargo.toml
/// ├── src/
/// ├── lib.rs
/// ├── er20.rs
/// ├── contract1.rs
/// ├── contract2.rs
/// ...
/// ```
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.write_to_crate(
/// "my-crate", "0.0.5", "./bindings", false
/// ).unwrap();
/// ```
pub fn write_to_crate(
self, self,
name: impl AsRef<str>, name: impl AsRef<str>,
version: impl AsRef<str>, version: impl AsRef<str>,
@ -379,39 +650,7 @@ serde_json = "1.0.79"
Ok(()) Ok(())
} }
/// This ensures that the already generated bindings crate matches the fn ensure_consistent_crate(
/// output of a fresh new run. Run this in a rust test, to get notified in
/// CI if the newly generated bindings deviate from the already generated
/// ones, and it's time to generate them again. This could happen if the
/// ABI of a contract or the output that `ethers` generates changed.
///
/// If this functions is run within a test during CI and fails, then it's
/// time to update all bindings.
///
/// # Returns
///
/// `Ok(())` if the freshly generated bindings match with the
/// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
/// with more information.
///
/// # Example
///
/// Check that the generated files are up to date
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// #[test]
/// fn generated_bindings_are_fresh() {
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
/// let abi_dir = project_root.join("abi");
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.ensure_consistent_crate(
/// "my-crate", "0.0.1", project_root.join("src/contracts"), false
/// ).expect("inconsistent bindings");
/// }
/// ```
pub fn ensure_consistent_crate(
self, self,
name: impl AsRef<str>, name: impl AsRef<str>,
version: impl AsRef<str>, version: impl AsRef<str>,
@ -428,48 +667,30 @@ serde_json = "1.0.79"
Ok(()) Ok(())
} }
/// This ensures that the already generated bindings module matches the fn ensure_consistent_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
/// output of a fresh new run. Run this in a rust test, to get notified in
/// CI if the newly generated bindings deviate from the already generated
/// ones, and it's time to generate them again. This could happen if the
/// ABI of a contract or the output that `ethers` generates changed.
///
/// If this functions is run within a test during CI and fails, then it's
/// time to update all bindings.
///
/// # Returns
///
/// `Ok(())` if the freshly generated bindings match with the
/// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
/// with more information.
///
/// # Example
///
/// Check that the generated files are up to date
///
/// ```no_run
/// # use ethers_contract_abigen::MultiAbigen;
/// #[test]
/// fn generated_bindings_are_fresh() {
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
/// let abi_dir = project_root.join("abi");
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
/// let bindings = gen.build().unwrap();
/// bindings.ensure_consistent_module(
/// project_root.join("src/contracts"), false
/// ).expect("inconsistent bindings");
/// }
/// ```
pub fn ensure_consistent_module(
self,
module: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.ensure_consistent_bindings(module, false, single_file)?; self.ensure_consistent_bindings(module, false, single_file)?;
Ok(()) Ok(())
} }
} }
/// Generate the shared prefix of the `lib.rs` or `mod.rs`
fn generate_prefix(mut buf: impl Write, is_crate: bool, single_file: bool) -> Result<()> {
writeln!(buf, "#![allow(clippy::all)]")?;
writeln!(
buf,
"//! This {} contains abigen! generated bindings for solidity contracts.",
if is_crate { "lib" } else { "module" }
)?;
writeln!(buf, "//! This is autogenerated code.")?;
writeln!(buf, "//! Do not manually edit these files.")?;
writeln!(
buf,
"//! {} may be overwritten by the codegen system at any time.",
if single_file && !is_crate { "This file" } else { "These files" }
)?;
Ok(())
}
fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> { fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> {
eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display()); eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display());
@ -493,6 +714,7 @@ fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
mod tests { mod tests {
use super::*; use super::*;
use ethers_solc::project_util::TempProject;
use std::{panic, path::PathBuf}; use std::{panic, path::PathBuf};
struct Context { struct Context {
@ -754,4 +976,82 @@ mod tests {
assert!(result, "Inconsistent bindings wrongly approved"); assert!(result, "Inconsistent bindings wrongly approved");
}) })
} }
#[test]
fn can_deduplicate_types() {
let tmp = TempProject::dapptools().unwrap();
tmp.add_source(
"Greeter",
r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
struct Inner {
bool a;
}
struct Stuff {
Inner inner;
}
contract Greeter1 {
function greet(Stuff calldata stuff) public pure returns (Stuff memory) {
return stuff;
}
}
contract Greeter2 {
function greet(Stuff calldata stuff) public pure returns (Stuff memory) {
return stuff;
}
}
"#,
)
.unwrap();
let _ = tmp.compile().unwrap();
let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap();
let bindings = gen.build().unwrap();
let single_file_dir = tmp.root().join("single_bindings");
bindings.write_to_module(&single_file_dir, true).unwrap();
let single_file_mod = single_file_dir.join("mod.rs");
assert!(single_file_mod.exists());
let content = fs::read_to_string(&single_file_mod).unwrap();
assert!(content.contains("mod __shared_types"));
assert!(content.contains("pub struct Inner"));
assert!(content.contains("pub struct Stuff"));
// multiple files
let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap();
let bindings = gen.build().unwrap();
let multi_file_dir = tmp.root().join("multi_bindings");
bindings.write_to_module(&multi_file_dir, false).unwrap();
let multi_file_mod = multi_file_dir.join("mod.rs");
assert!(multi_file_mod.exists());
let content = fs::read_to_string(&multi_file_mod).unwrap();
assert!(content.contains("pub mod shared_types"));
let greeter1 = multi_file_dir.join("greeter_1.rs");
assert!(greeter1.exists());
let content = fs::read_to_string(&greeter1).unwrap();
assert!(!content.contains("pub struct Inner"));
assert!(!content.contains("pub struct Stuff"));
let greeter2 = multi_file_dir.join("greeter_2.rs");
assert!(greeter2.exists());
let content = fs::read_to_string(&greeter2).unwrap();
assert!(!content.contains("pub struct Inner"));
assert!(!content.contains("pub struct Stuff"));
let shared_types = multi_file_dir.join("shared_types.rs");
assert!(shared_types.exists());
let content = fs::read_to_string(&shared_types).unwrap();
assert!(content.contains("pub struct Inner"));
assert!(content.contains("pub struct Stuff"));
}
} }

View File

@ -84,6 +84,9 @@ pub struct Item {
pub name: Option<String>, pub name: Option<String>,
#[serde(default)] #[serde(default)]
pub outputs: Vec<Component>, pub outputs: Vec<Component>,
// required to satisfy solidity events
#[serde(default, skip_serializing_if = "Option::is_none")]
pub anonymous: Option<bool>,
} }
/// Either an input/output or a nested component of an input/output /// Either an input/output or a nested component of an input/output
@ -97,11 +100,15 @@ pub struct Component {
pub type_field: String, pub type_field: String,
#[serde(default)] #[serde(default)]
pub components: Vec<Component>, pub components: Vec<Component>,
/// Indexed flag. for solidity events
#[serde(default, skip_serializing_if = "Option::is_none")]
pub indexed: Option<bool>,
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ethers_core::abi::Abi;
#[test] #[test]
fn can_parse_raw_abi() { fn can_parse_raw_abi() {
@ -123,4 +130,14 @@ mod tests {
let s = r#"[{"type":"function","name":"greet","inputs":[{"internalType":"struct Greeter.Stuff","name":"stuff","type":"tuple","components":[{"type":"bool"}]}],"outputs":[{"internalType":"struct Greeter.Stuff","name":"","type":"tuple","components":[{"type":"bool"}]}],"stateMutability":"view"}]"#; let s = r#"[{"type":"function","name":"greet","inputs":[{"internalType":"struct Greeter.Stuff","name":"stuff","type":"tuple","components":[{"type":"bool"}]}],"outputs":[{"internalType":"struct Greeter.Stuff","name":"","type":"tuple","components":[{"type":"bool"}]}],"stateMutability":"view"}]"#;
let _ = serde_json::from_str::<RawAbi>(s).unwrap(); let _ = serde_json::from_str::<RawAbi>(s).unwrap();
} }
#[test]
fn can_ethabi_round_trip() {
let s = r#"[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint64","name":"number","type":"uint64"}],"name":"MyEvent","type":"event"},{"inputs":[],"name":"greet","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#;
let raw = serde_json::from_str::<RawAbi>(s).unwrap();
let abi = serde_json::from_str::<Abi>(s).unwrap();
let de = serde_json::to_string(&raw).unwrap();
assert_eq!(abi, serde_json::from_str::<Abi>(&de).unwrap());
}
} }

View File

@ -5,13 +5,13 @@ use crate::spanned::{ParseInner, Spanned};
use ethers_contract_abigen::Abigen; use ethers_contract_abigen::Abigen;
use ethers_core::abi::{Function, FunctionExt, Param, StateMutability}; use ethers_core::abi::{Function, FunctionExt, Param, StateMutability};
use ethers_contract_abigen::contract::{Context, ExpandedContract}; use ethers_contract_abigen::{
use proc_macro2::{Span, TokenStream as TokenStream2}; contract::{Context, ExpandedContract},
use quote::{quote, ToTokens}; multi::MultiExpansion,
use std::{
collections::{HashMap, HashSet},
error::Error,
}; };
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::ToTokens;
use std::{collections::HashSet, error::Error};
use syn::{ use syn::{
braced, braced,
ext::IdentExt, ext::IdentExt,
@ -28,7 +28,6 @@ pub(crate) struct Contracts {
impl Contracts { impl Contracts {
pub(crate) fn expand(self) -> Result<TokenStream2, syn::Error> { pub(crate) fn expand(self) -> Result<TokenStream2, syn::Error> {
let mut tokens = TokenStream2::new();
let mut expansions = Vec::with_capacity(self.inner.len()); let mut expansions = Vec::with_capacity(self.inner.len());
// expand all contracts // expand all contracts
@ -38,56 +37,14 @@ impl Contracts {
expansions.push(contract); expansions.push(contract);
} }
// merge all types if more than 1 contract // expand all contract expansions
if expansions.len() > 1 { Ok(MultiExpansion::new(expansions).expand_inplace())
// check for type conflicts
let mut conflicts: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, (_, ctx)) in expansions.iter().enumerate() {
for type_identifier in ctx.internal_structs().rust_type_names().keys() {
conflicts
.entry(type_identifier.clone())
.or_insert_with(|| Vec::with_capacity(1))
.push(idx);
}
}
let mut shared_types = TokenStream2::new();
let shared_types_mdoule = quote!(__shared_types);
let mut dirty = HashSet::new();
// resolve type conflicts
for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
// extract the shared type once
shared_types.extend(expansions[contracts[0]].1.struct_definition(id).unwrap());
// remove the shared type
for contract in contracts.iter().copied() {
expansions[contract].1.remove_struct(id);
dirty.insert(contract);
}
}
// regenerate all struct definitions that were hit and adjust imports
for contract in dirty {
let (expanded, ctx) = &mut expansions[contract];
expanded.abi_structs = ctx.abi_structs().unwrap();
expanded.imports.extend(quote!( pub use super::#shared_types_mdoule::*;));
}
tokens.extend(quote! {
pub mod #shared_types_mdoule {
#shared_types
}
});
}
tokens.extend(expansions.into_iter().map(|(exp, _)| exp.into_tokens()));
Ok(tokens)
} }
fn expand_contract( fn expand_contract(
contract: ContractArgs, contract: ContractArgs,
) -> Result<(ExpandedContract, Context), Box<dyn Error>> { ) -> Result<(ExpandedContract, Context), Box<dyn Error>> {
let contract = contract.into_builder()?; Ok(contract.into_builder()?.expand()?)
let ctx = Context::from_abigen(contract)?;
Ok((ctx.expand()?, ctx))
} }
} }