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