1use alloy_json_abi::ToSolConfig;
13use alloy_sol_macro_expander::expand::expand;
14use alloy_sol_macro_input::{SolInput, SolInputKind};
15use eyre::{Context, OptionExt, Result};
16use foundry_common::fs;
17use proc_macro2::{Span, TokenStream};
18use rayon::prelude::*;
19use std::{
20 fmt::Write,
21 io::{BufWriter, Write as IoWrite},
22 path::{Path, PathBuf},
23};
24
25use heck::ToSnakeCase;
26
27const SERDE_ARRAY_IMPL_MAX_LEN: usize = 32;
28const SERDE_WITH_DEP: &str =
29 r#"serde_with = { version = "3.15", default-features = false, features = ["std"] }"#;
30const BINDINGS_HEADER: &str = r#"#![allow(unused_imports, unused_attributes, clippy::all, rustdoc::all)]
31//! This module contains the sol! generated bindings for solidity contracts.
32//! This is autogenerated code.
33//! Do not manually edit these files.
34//! These files may be overwritten by the codegen system at any time.
35"#;
36
37pub struct SolMacroGen {
38 pub path: PathBuf,
39 pub name: String,
40 pub expansion: Option<String>,
41 needs_serde_with: bool,
42}
43
44impl SolMacroGen {
45 pub const fn new(path: PathBuf, name: String) -> Self {
46 Self { path, name, expansion: None, needs_serde_with: false }
47 }
48
49 pub fn get_sol_input(&self) -> Result<SolInput> {
50 let path = self.path.to_string_lossy().into_owned();
51 let name: syn::Ident = syn::parse_str(&self.name).wrap_err_with(|| {
52 format!("`{}` is not a valid Rust identifier for generated bindings", self.name)
53 })?;
54 let tokens = quote::quote! {
55 #[sol(ignore_unlinked)]
56 #name,
57 #path
58 };
59
60 let sol_input: SolInput = syn::parse2(tokens).wrap_err("failed to parse input")?;
61
62 Ok(sol_input)
63 }
64}
65
66pub struct MultiSolMacroGen {
67 pub instances: Vec<SolMacroGen>,
68}
69
70impl MultiSolMacroGen {
71 pub const fn new(instances: Vec<SolMacroGen>) -> Self {
72 Self { instances }
73 }
74
75 pub fn populate_expansion(&mut self, bindings_path: &Path) -> Result<()> {
76 for instance in &mut self.instances {
77 let path = bindings_path.join(format!("{}.rs", instance.name.to_snake_case()));
78 let expansion = fs::read_to_string(path).wrap_err("Failed to read file")?;
79 expansion
80 .parse::<TokenStream>()
81 .map_err(|e| eyre::eyre!("Failed to parse TokenStream: {e}"))?;
82 instance.expansion = Some(expansion);
83 }
84 Ok(())
85 }
86
87 pub fn generate_bindings(&mut self, all_derives: bool, sol_config: &ToSolConfig) -> Result<()> {
88 self.instances.par_iter_mut().try_for_each(|instance| {
89 Self::generate_binding(instance, all_derives, sol_config).wrap_err_with(|| {
90 format!(
91 "failed to generate bindings for {}:{}",
92 instance.path.display(),
93 instance.name
94 )
95 })
96 })
97 }
98
99 fn generate_binding(
100 instance: &mut SolMacroGen,
101 all_derives: bool,
102 sol_config: &ToSolConfig,
103 ) -> Result<()> {
104 let input = instance.get_sol_input()?.normalize_json_with_config(sol_config.clone())?;
105 let SolInput { attrs: _, path: _, kind } = input;
106
107 let tokens = match kind {
108 SolInputKind::Sol(mut file) => {
109 let sol_attr: syn::Attribute = if all_derives {
110 syn::parse_quote! {
111 #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract =
112 alloy::contract, all_derives = true, extra_derives(serde::Serialize,
113 serde::Deserialize))] }
114 } else {
115 syn::parse_quote! {
116 #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract =
117 alloy::contract)] }
118 };
119 file.attrs.push(sol_attr);
120 expand(file).wrap_err("failed to expand")?
121 }
122 _ => unreachable!(),
123 };
124
125 let (tokens, needs_serde_with) =
126 if all_derives { add_large_array_serde_attrs(tokens)? } else { (tokens, false) };
127
128 let file = syn::parse2(tokens).wrap_err("failed to parse generated tokens as an AST")?;
129 instance.expansion =
130 Some(qualify_shadowed_sibling_module_paths(prettyplease::unparse(&file)));
131 instance.needs_serde_with = needs_serde_with;
132 Ok(())
133 }
134
135 #[allow(clippy::too_many_arguments)]
136 pub fn write_to_crate(
137 &mut self,
138 name: &str,
139 version: &str,
140 description: &str,
141 license: &str,
142 bindings_path: &Path,
143 single_file: bool,
144 alloy_version: Option<String>,
145 alloy_rev: Option<String>,
146 all_derives: bool,
147 sol_config: &ToSolConfig,
148 ) -> Result<()> {
149 self.generate_bindings(all_derives, sol_config)?;
150
151 let src = bindings_path.join("src");
152 fs::create_dir_all(&src)?;
153
154 let cargo_toml_path = bindings_path.join("Cargo.toml");
156 let mut toml_contents = format!(
157 r#"[package]
158name = "{name}"
159version = "{version}"
160edition = "2021"
161"#
162 );
163
164 if !description.is_empty() {
165 toml_contents.push_str(&format!("description = \"{description}\"\n"));
166 }
167
168 if !license.is_empty() {
169 let formatted_licenses: Vec<String> =
170 license.split(',').map(Self::parse_license_alias).collect();
171
172 let formatted_license = formatted_licenses.join(" OR ");
173 toml_contents.push_str(&format!("license = \"{formatted_license}\"\n"));
174 }
175
176 toml_contents.push_str("\n[dependencies]\n");
177
178 let alloy_dep = Self::get_alloy_dep(alloy_version, alloy_rev);
179 write!(toml_contents, "{alloy_dep}")?;
180
181 if all_derives {
182 let serde_dep = r#"serde = { version = "1.0", features = ["derive"] }"#;
183 write!(toml_contents, "\n{serde_dep}")?;
184 if self.instances.iter().any(|instance| instance.needs_serde_with) {
185 write!(toml_contents, "\n{SERDE_WITH_DEP}")?;
186 }
187 }
188
189 fs::write(cargo_toml_path, toml_contents).wrap_err("Failed to write Cargo.toml")?;
190
191 let lib_path = src.join("lib.rs");
192 if single_file {
193 write_single_file(&lib_path, &self.instances).wrap_err("Failed to write lib.rs")?;
194 return Ok(());
195 }
196
197 let mut lib_contents = BINDINGS_HEADER.to_string();
198 for instance in &self.instances {
199 let contents = instance.expansion.as_ref().unwrap();
200 let name = instance.name.to_snake_case();
201 let path = src.join(format!("{name}.rs"));
202 fs::write(path, contents).wrap_err("failed to write to file")?;
203 write_mod_name(&mut lib_contents, &name)?;
204 }
205
206 let lib_file = syn::parse_file(&lib_contents).wrap_err(
207 "failed to parse generated tokens as an AST for lib.rs;\nthis is likely a bug",
208 )?;
209 let lib_contents = prettyplease::unparse(&lib_file);
210 fs::write(lib_path, lib_contents).wrap_err("Failed to write lib.rs")?;
211
212 Ok(())
213 }
214
215 pub fn parse_license_alias(license: &str) -> String {
217 match license.trim().to_lowercase().as_str() {
218 "mit" => "MIT".to_string(),
219 "apache" | "apache2" | "apache20" | "apache2.0" => "Apache-2.0".to_string(),
220 "gpl" | "gpl3" => "GPL-3.0".to_string(),
221 "lgpl" | "lgpl3" => "LGPL-3.0".to_string(),
222 "agpl" | "agpl3" => "AGPL-3.0".to_string(),
223 "bsd" | "bsd3" => "BSD-3-Clause".to_string(),
224 "bsd2" => "BSD-2-Clause".to_string(),
225 "mpl" | "mpl2" => "MPL-2.0".to_string(),
226 "isc" => "ISC".to_string(),
227 "unlicense" => "Unlicense".to_string(),
228 _ => license.trim().to_string(),
229 }
230 }
231
232 pub fn write_to_module(
233 &mut self,
234 bindings_path: &Path,
235 single_file: bool,
236 all_derives: bool,
237 sol_config: &ToSolConfig,
238 ) -> Result<()> {
239 self.generate_bindings(all_derives, sol_config)?;
240
241 fs::create_dir_all(bindings_path)?;
242
243 let mod_path = bindings_path.join("mod.rs");
244 if single_file {
245 write_single_file(&mod_path, &self.instances).wrap_err("Failed to write mod.rs")?;
246 return Ok(());
247 }
248
249 let mut mod_contents = BINDINGS_HEADER.to_string();
250 for instance in &self.instances {
251 let name = instance.name.to_snake_case();
252 write_mod_name(&mut mod_contents, &name)?;
253 fs::write(
254 bindings_path.join(format!("{name}.rs")),
255 instance.expansion.as_ref().unwrap(),
256 )
257 .wrap_err("Failed to write file")?;
258 }
259
260 let mod_file = syn::parse_file(&mod_contents)?;
261 let mod_contents = qualify_shadowed_sibling_module_paths(prettyplease::unparse(&mod_file));
262
263 fs::write(mod_path, mod_contents).wrap_err("Failed to write mod.rs")?;
264
265 Ok(())
266 }
267
268 #[expect(clippy::too_many_arguments)]
274 pub fn check_consistency(
275 &self,
276 name: &str,
277 version: &str,
278 crate_path: &Path,
279 single_file: bool,
280 check_cargo_toml: bool,
281 is_mod: bool,
282 alloy_version: Option<String>,
283 alloy_rev: Option<String>,
284 ) -> Result<()> {
285 if check_cargo_toml && !is_mod {
286 self.check_cargo_toml(name, version, crate_path, alloy_version, alloy_rev)?;
287 }
288
289 let mut super_contents = BINDINGS_HEADER.to_string();
290 if single_file {
291 for instance in &self.instances {
292 let contents = instance
293 .expansion
294 .as_ref()
295 .ok_or_eyre(format!("TokenStream for {} does not exist", instance.name))?;
296 super_contents.push_str(contents);
297 }
298 } else {
299 for instance in &self.instances {
300 let name = instance.name.to_snake_case();
301 let path = if is_mod {
302 crate_path.join(format!("{name}.rs"))
303 } else {
304 crate_path.join(format!("src/{name}.rs"))
305 };
306 let contents = instance
307 .expansion
308 .as_ref()
309 .ok_or_eyre(format!("TokenStream for {path:?} does not exist"))?;
310
311 self.check_file_contents(&path, contents)?;
312 write_mod_name(&mut super_contents, &name)?;
313 }
314 }
315
316 let super_path =
317 if is_mod { crate_path.join("mod.rs") } else { crate_path.join("src/lib.rs") };
318 self.check_file_contents(&super_path, &super_contents)?;
319
320 Ok(())
321 }
322
323 fn check_file_contents(&self, file_path: &Path, expected_contents: &str) -> Result<()> {
324 eyre::ensure!(file_path.is_file(), "{} is not a file", file_path.display());
325 let file_contents = fs::read_to_string(file_path).wrap_err("Failed to read file")?;
326
327 if file_contents == expected_contents {
329 return Ok(());
330 }
331
332 let formatted_file = prettyplease::unparse(&syn::parse_file(&file_contents)?);
335 let formatted_exp = prettyplease::unparse(&syn::parse_file(expected_contents)?);
336
337 eyre::ensure!(
338 formatted_file == formatted_exp,
339 "File contents do not match expected contents for \"{}\"",
340 file_path.display()
341 );
342 Ok(())
343 }
344
345 fn check_cargo_toml(
346 &self,
347 name: &str,
348 version: &str,
349 crate_path: &Path,
350 alloy_version: Option<String>,
351 alloy_rev: Option<String>,
352 ) -> Result<()> {
353 eyre::ensure!(crate_path.is_dir(), "Crate path must be a directory");
354
355 let cargo_toml_path = crate_path.join("Cargo.toml");
356
357 eyre::ensure!(cargo_toml_path.is_file(), "Cargo.toml must exist");
358 let cargo_toml_contents =
359 fs::read_to_string(cargo_toml_path).wrap_err("Failed to read Cargo.toml")?;
360
361 let name_check = format!("name = \"{name}\"");
362 let version_check = format!("version = \"{version}\"");
363 let alloy_dep_check = Self::get_alloy_dep(alloy_version, alloy_rev);
364 let serde_with_consistent =
365 !self.instances.iter().any(|instance| instance.needs_serde_with)
366 || cargo_toml_contents.contains(SERDE_WITH_DEP);
367 let toml_consistent = cargo_toml_contents.contains(&name_check)
368 && cargo_toml_contents.contains(&version_check)
369 && cargo_toml_contents.contains(&alloy_dep_check)
370 && serde_with_consistent;
371 eyre::ensure!(
372 toml_consistent,
373 r#"The contents of Cargo.toml do not match the expected output of the latest `sol!` version.
374 This indicates that the existing bindings are outdated and need to be generated again."#
375 );
376
377 Ok(())
378 }
379
380 fn get_alloy_dep(alloy_version: Option<String>, alloy_rev: Option<String>) -> String {
384 if let Some(alloy_version) = alloy_version {
385 format!(
386 r#"alloy = {{ version = "{alloy_version}", features = ["sol-types", "contract"] }}"#,
387 )
388 } else if let Some(alloy_rev) = alloy_rev {
389 format!(
390 r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{alloy_rev}", features = ["sol-types", "contract"] }}"#,
391 )
392 } else {
393 r#"alloy = { version = "1.0", features = ["sol-types", "contract"] }"#.to_string()
394 }
395 }
396}
397
398fn write_single_file(path: &Path, instances: &[SolMacroGen]) -> Result<()> {
400 let file = fs::create_file(path)?;
401 let mut writer = BufWriter::new(file);
402 writer.write_all(BINDINGS_HEADER.as_bytes())?;
403 for instance in instances {
404 writer.write_all(instance.expansion.as_ref().unwrap().as_bytes())?;
405 }
406 writer.flush()?;
407 Ok(())
408}
409
410#[derive(Default)]
411struct LargeArraySerdeAttrs {
412 added: bool,
413}
414
415impl syn::visit_mut::VisitMut for LargeArraySerdeAttrs {
416 fn visit_item_struct_mut(&mut self, item: &mut syn::ItemStruct) {
417 for field in &mut item.fields {
418 if let Some(adapter) = large_array_serde_adapter(&field.ty) {
419 let adapter =
420 syn::LitStr::new(&format!("::serde_with::As::<{adapter}>"), Span::call_site());
421 field.attrs.insert(0, syn::parse_quote!(#[serde(with = #adapter)]));
422 self.added = true;
423 }
424 }
425 }
426}
427
428fn add_large_array_serde_attrs(tokens: TokenStream) -> Result<(TokenStream, bool)> {
430 let mut file = syn::parse2::<syn::File>(tokens)
431 .wrap_err("failed to parse generated bindings for large array support")?;
432 let mut visitor = LargeArraySerdeAttrs::default();
433 syn::visit_mut::VisitMut::visit_file_mut(&mut visitor, &mut file);
434 Ok((quote::quote!(#file), visitor.added))
435}
436
437fn large_array_serde_adapter(ty: &syn::Type) -> Option<String> {
438 match ty {
439 syn::Type::Array(array) => {
440 let syn::Expr::Lit(expr) = &array.len else { return None };
441 let syn::Lit::Int(length) = &expr.lit else { return None };
442 let element = large_array_serde_adapter(&array.elem);
443 let is_large =
444 length.base10_parse::<usize>().is_ok_and(|len| len > SERDE_ARRAY_IMPL_MAX_LEN);
445 if !is_large && element.is_none() {
446 return None;
447 }
448
449 let element = element.unwrap_or_else(|| "::serde_with::Same".to_string());
450 Some(format!("[{element}; {}]", length.base10_digits()))
451 }
452 syn::Type::Path(type_path) if type_path.qself.is_none() => {
453 let last = type_path.path.segments.last()?;
454 if last.ident == "Vec"
455 && let syn::PathArguments::AngleBracketed(arguments) = &last.arguments
456 && arguments.args.len() == 1
457 && let Some(syn::GenericArgument::Type(element)) = arguments.args.first()
458 && let Some(element) = large_array_serde_adapter(element)
459 {
460 Some(format!("::std::vec::Vec<{element}>"))
461 } else {
462 None
463 }
464 }
465 _ => None,
466 }
467}
468
469fn write_mod_name(contents: &mut String, name: &str) -> Result<()> {
470 if syn::parse_str::<syn::Ident>(name).is_ok() {
471 write!(contents, "pub mod {name};")?;
472 } else {
473 write!(contents, "pub mod r#{name};")?;
474 }
475 Ok(())
476}
477
478fn qualify_shadowed_sibling_module_paths(mut contents: String) -> String {
487 let module_names = top_level_module_names(&contents);
488 let enum_names = public_enum_names(&contents);
489
490 for module_name in module_names {
491 if enum_names.iter().any(|enum_name| enum_name == &module_name) {
492 contents = qualify_unqualified_module_paths(&contents, &module_name);
493 }
494 }
495
496 contents
497}
498
499fn qualify_unqualified_module_paths(contents: &str, module_name: &str) -> String {
500 let needle = format!("{module_name}::");
501 let replacement = format!("super::{module_name}::");
502 let mut qualified = String::with_capacity(contents.len());
503 let mut rest = contents;
504
505 while let Some(index) = rest.find(&needle) {
506 let (before, after) = rest.split_at(index);
507 qualified.push_str(before);
508
509 let boundary = before
510 .chars()
511 .next_back()
512 .is_none_or(|c| !matches!(c, '_' | '0'..='9' | 'a'..='z' | 'A'..='Z' | ':'));
513
514 if boundary {
515 qualified.push_str(&replacement);
516 } else {
517 qualified.push_str(&needle);
518 }
519
520 rest = &after[needle.len()..];
521 }
522
523 qualified.push_str(rest);
524 qualified
525}
526
527fn top_level_module_names(contents: &str) -> Vec<String> {
528 contents
529 .split("pub mod ")
530 .skip(1)
531 .filter_map(|rest| rest.split_whitespace().next())
532 .map(|name| name.trim_start_matches("r#").to_string())
533 .collect()
534}
535
536fn public_enum_names(contents: &str) -> Vec<String> {
537 contents
538 .split("pub enum ")
539 .skip(1)
540 .filter_map(|rest| rest.split_whitespace().next())
541 .map(|name| name.trim_start_matches("r#").to_string())
542 .collect()
543}
544
545#[cfg(test)]
546mod tests {
547 use super::large_array_serde_adapter;
548
549 fn adapter(ty: &str) -> Option<String> {
550 large_array_serde_adapter(&syn::parse_str(ty).unwrap())
551 }
552
553 #[test]
554 fn builds_large_array_serde_adapters() {
555 assert_eq!(adapter("[u64; 32]"), None);
556 assert_eq!(adapter("[u64; 33]"), Some("[::serde_with::Same; 33]".to_string()));
557 assert_eq!(adapter("[[u64; 48]; 2]"), Some("[[::serde_with::Same; 48]; 2]".to_string()));
558 assert_eq!(
559 adapter("alloy::sol_types::private::Vec<[u64; 48]>"),
560 Some("::std::vec::Vec<[::serde_with::Same; 48]>".to_string())
561 );
562 }
563
564 #[test]
565 fn get_sol_input_rejects_invalid_identifier_instead_of_panicking() {
566 let instance =
568 super::SolMacroGen::new(std::path::PathBuf::from("Foo.json"), "Foo$Bar".to_string());
569 assert!(instance.get_sol_input().is_err());
570 }
571}