use alloy_sol_macro_expander::expand::expand;
use alloy_sol_macro_input::{SolInput, SolInputKind};
use eyre::{Context, OptionExt, Result};
use foundry_common::fs;
use proc_macro2::{Span, TokenStream};
use std::{
fmt::Write,
path::{Path, PathBuf},
str::FromStr,
};
pub struct SolMacroGen {
pub path: PathBuf,
pub name: String,
pub expansion: Option<TokenStream>,
}
impl SolMacroGen {
pub fn new(path: PathBuf, name: String) -> Self {
Self { path, name, expansion: None }
}
pub fn get_sol_input(&self) -> Result<SolInput> {
let path = self.path.to_string_lossy().into_owned();
let name = proc_macro2::Ident::new(&self.name, Span::call_site());
let tokens = quote::quote! {
#name,
#path
};
let sol_input: SolInput = syn::parse2(tokens).wrap_err("failed to parse input")?;
Ok(sol_input)
}
}
pub struct MultiSolMacroGen {
pub artifacts_path: PathBuf,
pub instances: Vec<SolMacroGen>,
}
impl MultiSolMacroGen {
pub fn new(artifacts_path: &Path, instances: Vec<SolMacroGen>) -> Self {
Self { artifacts_path: artifacts_path.to_path_buf(), instances }
}
pub fn populate_expansion(&mut self, bindings_path: &Path) -> Result<()> {
for instance in &mut self.instances {
let path = bindings_path.join(format!("{}.rs", instance.name.to_lowercase()));
let expansion = fs::read_to_string(path).wrap_err("Failed to read file")?;
let tokens = TokenStream::from_str(&expansion)
.map_err(|e| eyre::eyre!("Failed to parse TokenStream: {e}"))?;
instance.expansion = Some(tokens);
}
Ok(())
}
pub fn generate_bindings(&mut self) -> Result<()> {
for instance in &mut self.instances {
Self::generate_binding(instance).wrap_err_with(|| {
format!(
"failed to generate bindings for {}:{}",
instance.path.display(),
instance.name
)
})?;
}
Ok(())
}
fn generate_binding(instance: &mut SolMacroGen) -> Result<()> {
let input = instance.get_sol_input()?.normalize_json()?;
let SolInput { attrs: _, path: _, kind } = input;
let tokens = match kind {
SolInputKind::Sol(mut file) => {
let sol_attr: syn::Attribute = syn::parse_quote! {
#[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract = alloy::contract)]
};
file.attrs.push(sol_attr);
expand(file).wrap_err("failed to expand")?
}
_ => unreachable!(),
};
instance.expansion = Some(tokens);
Ok(())
}
pub fn write_to_crate(
&mut self,
name: &str,
version: &str,
bindings_path: &Path,
single_file: bool,
alloy_version: Option<String>,
) -> Result<()> {
self.generate_bindings()?;
let src = bindings_path.join("src");
let _ = fs::create_dir_all(&src);
let cargo_toml_path = bindings_path.join("Cargo.toml");
let mut toml_contents = format!(
r#"[package]
name = "{name}"
version = "{version}"
edition = "2021"
[dependencies]
"#
);
let alloy_dep = if let Some(alloy_version) = alloy_version {
format!(
r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{alloy_version}", features = ["sol-types", "contract"] }}"#
)
} else {
r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
};
write!(toml_contents, "{alloy_dep}")?;
fs::write(cargo_toml_path, toml_contents).wrap_err("Failed to write Cargo.toml")?;
let mut lib_contents = String::new();
write!(
&mut lib_contents,
r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
//! This module contains the sol! generated bindings for solidity contracts.
//! This is autogenerated code.
//! Do not manually edit these files.
//! These files may be overwritten by the codegen system at any time.
"#
)?;
let parse_error = |name: &str| {
format!("failed to parse generated tokens as an AST for {name};\nthis is likely a bug")
};
for instance in &self.instances {
let contents = instance.expansion.as_ref().unwrap();
let name = instance.name.to_lowercase();
let path = src.join(format!("{name}.rs"));
let file = syn::parse2(contents.clone())
.wrap_err_with(|| parse_error(&format!("{}:{}", path.display(), name)))?;
let contents = prettyplease::unparse(&file);
if single_file {
write!(&mut lib_contents, "{contents}")?;
} else {
fs::write(path, contents).wrap_err("failed to write to file")?;
writeln!(&mut lib_contents, "pub mod {name};")?;
}
}
let lib_path = src.join("lib.rs");
let lib_file = syn::parse_file(&lib_contents).wrap_err_with(|| parse_error("lib.rs"))?;
let lib_contents = prettyplease::unparse(&lib_file);
fs::write(lib_path, lib_contents).wrap_err("Failed to write lib.rs")?;
Ok(())
}
pub fn write_to_module(&mut self, bindings_path: &Path, single_file: bool) -> Result<()> {
self.generate_bindings()?;
let _ = fs::create_dir_all(bindings_path);
let mut mod_contents = r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
//! This module contains the sol! generated bindings for solidity contracts.
//! This is autogenerated code.
//! Do not manually edit these files.
//! These files may be overwritten by the codegen system at any time.
"#
.to_string();
for instance in &self.instances {
let name = instance.name.to_lowercase();
if !single_file {
write!(
mod_contents,
r#"pub mod {};
"#,
instance.name.to_lowercase()
)?;
let mut contents = String::new();
write!(contents, "{}", instance.expansion.as_ref().unwrap())?;
let file = syn::parse_file(&contents)?;
let contents = prettyplease::unparse(&file);
fs::write(bindings_path.join(format!("{name}.rs")), contents)
.wrap_err("Failed to write file")?;
} else {
let mut contents = String::new();
write!(contents, "{}\n\n", instance.expansion.as_ref().unwrap())?;
write!(mod_contents, "{contents}")?;
}
}
let mod_path = bindings_path.join("mod.rs");
let mod_file = syn::parse_file(&mod_contents)?;
let mod_contents = prettyplease::unparse(&mod_file);
fs::write(mod_path, mod_contents).wrap_err("Failed to write mod.rs")?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn check_consistency(
&self,
name: &str,
version: &str,
crate_path: &Path,
single_file: bool,
check_cargo_toml: bool,
is_mod: bool,
alloy_version: Option<String>,
) -> Result<()> {
if check_cargo_toml {
self.check_cargo_toml(name, version, crate_path, alloy_version)?;
}
let mut super_contents = String::new();
write!(
&mut super_contents,
r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
//! This module contains the sol! generated bindings for solidity contracts.
//! This is autogenerated code.
//! Do not manually edit these files.
//! These files may be overwritten by the codegen system at any time.
"#
)?;
if !single_file {
for instance in &self.instances {
let name = instance.name.to_lowercase();
let path = if is_mod {
crate_path.join(format!("{name}.rs"))
} else {
crate_path.join(format!("src/{name}.rs"))
};
let tokens = instance
.expansion
.as_ref()
.ok_or_eyre(format!("TokenStream for {path:?} does not exist"))?
.to_string();
self.check_file_contents(&path, &tokens)?;
write!(
&mut super_contents,
r#"pub mod {name};
"#
)?;
}
let super_path =
if is_mod { crate_path.join("mod.rs") } else { crate_path.join("src/lib.rs") };
self.check_file_contents(&super_path, &super_contents)?;
}
Ok(())
}
fn check_file_contents(&self, file_path: &Path, expected_contents: &str) -> Result<()> {
eyre::ensure!(
file_path.is_file() && file_path.exists(),
"{} is not a file",
file_path.display()
);
let file_contents = &fs::read_to_string(file_path).wrap_err("Failed to read file")?;
let file_contents = syn::parse_file(file_contents)?;
let formatted_file = prettyplease::unparse(&file_contents);
let expected_contents = syn::parse_file(expected_contents)?;
let formatted_exp = prettyplease::unparse(&expected_contents);
eyre::ensure!(
formatted_file == formatted_exp,
"File contents do not match expected contents for {file_path:?}"
);
Ok(())
}
fn check_cargo_toml(
&self,
name: &str,
version: &str,
crate_path: &Path,
alloy_version: Option<String>,
) -> Result<()> {
eyre::ensure!(crate_path.is_dir(), "Crate path must be a directory");
let cargo_toml_path = crate_path.join("Cargo.toml");
eyre::ensure!(cargo_toml_path.is_file(), "Cargo.toml must exist");
let cargo_toml_contents =
fs::read_to_string(cargo_toml_path).wrap_err("Failed to read Cargo.toml")?;
let name_check = format!("name = \"{name}\"");
let version_check = format!("version = \"{version}\"");
let alloy_dep_check = if let Some(version) = alloy_version {
format!(
r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{version}", features = ["sol-types", "contract"] }}"#,
)
} else {
r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
};
let toml_consistent = cargo_toml_contents.contains(&name_check) &&
cargo_toml_contents.contains(&version_check) &&
cargo_toml_contents.contains(&alloy_dep_check);
eyre::ensure!(
toml_consistent,
r#"The contents of Cargo.toml do not match the expected output of the latest `sol!` version.
This indicates that the existing bindings are outdated and need to be generated again."#
);
Ok(())
}
}