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 std::{
18 fmt::Write,
19 path::{Path, PathBuf},
20 str::FromStr,
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<TokenStream>,
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
70 let tokens = TokenStream::from_str(&expansion)
71 .map_err(|e| eyre::eyre!("Failed to parse TokenStream: {e}"))?;
72 instance.expansion = Some(tokens);
73 }
74 Ok(())
75 }
76
77 pub fn generate_bindings(&mut self, all_derives: bool) -> Result<()> {
78 for instance in &mut self.instances {
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 Ok(())
89 }
90
91 fn generate_binding(instance: &mut SolMacroGen, all_derives: bool) -> Result<()> {
92 let input = instance.get_sol_input()?.normalize_json()?;
93 let SolInput { attrs: _, path: _, kind } = input;
94
95 let tokens = match kind {
96 SolInputKind::Sol(mut file) => {
97 let sol_attr: syn::Attribute = if all_derives {
98 syn::parse_quote! {
99 #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract =
100 alloy::contract, all_derives = true, extra_derives(serde::Serialize,
101 serde::Deserialize))] }
102 } else {
103 syn::parse_quote! {
104 #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract =
105 alloy::contract)] }
106 };
107 file.attrs.push(sol_attr);
108 expand(file).wrap_err("failed to expand")?
109 }
110 _ => unreachable!(),
111 };
112
113 let (tokens, needs_serde_with) =
114 if all_derives { add_large_array_serde_attrs(tokens)? } else { (tokens, false) };
115
116 instance.expansion = Some(tokens);
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 let parse_error = |name: &str| {
189 format!("failed to parse generated tokens as an AST for {name};\nthis is likely a bug")
190 };
191 for instance in &self.instances {
192 let contents = instance.expansion.as_ref().unwrap();
193
194 let name = instance.name.to_snake_case();
195 let path = src.join(format!("{name}.rs"));
196 let file = syn::parse2(contents.clone())
197 .wrap_err_with(|| parse_error(&format!("{}:{}", path.display(), name)))?;
198 let contents = qualify_shadowed_sibling_module_paths(prettyplease::unparse(&file));
199 if single_file {
200 write!(&mut lib_contents, "{contents}")?;
201 } else {
202 fs::write(path, contents).wrap_err("failed to write to file")?;
203 write_mod_name(&mut lib_contents, &name)?;
204 }
205 }
206
207 let lib_path = src.join("lib.rs");
208 let lib_file = syn::parse_file(&lib_contents).wrap_err_with(|| parse_error("lib.rs"))?;
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 ) -> Result<()> {
238 self.generate_bindings(all_derives)?;
239
240 fs::create_dir_all(bindings_path)?;
241
242 let mut mod_contents =
243 r#"#![allow(unused_imports, unused_attributes, clippy::all, rustdoc::all)]
244 //! This module contains the sol! generated bindings for solidity contracts.
245 //! This is autogenerated code.
246 //! Do not manually edit these files.
247 //! These files may be overwritten by the codegen system at any time.
248 "#
249 .to_string();
250
251 for instance in &self.instances {
252 let name = instance.name.to_snake_case();
253 if single_file {
254 let mut contents = String::new();
256 write!(contents, "{}\n\n", instance.expansion.as_ref().unwrap())?;
257 write!(mod_contents, "{contents}")?;
258 } else {
259 write_mod_name(&mut mod_contents, &name)?;
261 let mut contents = String::new();
262
263 write!(contents, "{}", instance.expansion.as_ref().unwrap())?;
264 let file = syn::parse_file(&contents)?;
265
266 let contents = qualify_shadowed_sibling_module_paths(prettyplease::unparse(&file));
267 fs::write(bindings_path.join(format!("{name}.rs")), contents)
268 .wrap_err("Failed to write file")?;
269 }
270 }
271
272 let mod_path = bindings_path.join("mod.rs");
273 let mod_file = syn::parse_file(&mod_contents)?;
274 let mod_contents = qualify_shadowed_sibling_module_paths(prettyplease::unparse(&mod_file));
275
276 fs::write(mod_path, mod_contents).wrap_err("Failed to write mod.rs")?;
277
278 Ok(())
279 }
280
281 #[expect(clippy::too_many_arguments)]
287 pub fn check_consistency(
288 &self,
289 name: &str,
290 version: &str,
291 crate_path: &Path,
292 single_file: bool,
293 check_cargo_toml: bool,
294 is_mod: bool,
295 alloy_version: Option<String>,
296 alloy_rev: Option<String>,
297 ) -> Result<()> {
298 if check_cargo_toml && !is_mod {
299 self.check_cargo_toml(name, version, crate_path, alloy_version, alloy_rev)?;
300 }
301
302 let mut super_contents = String::new();
303 write!(
304 &mut super_contents,
305 r#"#![allow(unused_imports, unused_attributes, clippy::all, rustdoc::all)]
306 //! This module contains the sol! generated bindings for solidity contracts.
307 //! This is autogenerated code.
308 //! Do not manually edit these files.
309 //! These files may be overwritten by the codegen system at any time.
310 "#
311 )?;
312 if !single_file {
313 for instance in &self.instances {
314 let name = instance.name.to_snake_case();
315 let path = if is_mod {
316 crate_path.join(format!("{name}.rs"))
317 } else {
318 crate_path.join(format!("src/{name}.rs"))
319 };
320 let tokens = instance
321 .expansion
322 .as_ref()
323 .ok_or_eyre(format!("TokenStream for {path:?} does not exist"))?
324 .to_string();
325
326 self.check_file_contents(&path, &tokens)?;
327 write_mod_name(&mut super_contents, &name)?;
328 }
329
330 let super_path =
331 if is_mod { crate_path.join("mod.rs") } else { crate_path.join("src/lib.rs") };
332 self.check_file_contents(&super_path, &super_contents)?;
333 }
334
335 Ok(())
336 }
337
338 fn check_file_contents(&self, file_path: &Path, expected_contents: &str) -> Result<()> {
339 eyre::ensure!(file_path.is_file(), "{} is not a file", file_path.display());
340 let file_contents = &fs::read_to_string(file_path).wrap_err("Failed to read file")?;
341
342 let file_contents = syn::parse_file(file_contents)?;
344 let formatted_file = prettyplease::unparse(&file_contents);
345
346 let expected_contents = syn::parse_file(expected_contents)?;
347 let formatted_exp =
348 qualify_shadowed_sibling_module_paths(prettyplease::unparse(&expected_contents));
349
350 eyre::ensure!(
351 formatted_file == formatted_exp,
352 "File contents do not match expected contents for {file_path:?}"
353 );
354 Ok(())
355 }
356
357 fn check_cargo_toml(
358 &self,
359 name: &str,
360 version: &str,
361 crate_path: &Path,
362 alloy_version: Option<String>,
363 alloy_rev: Option<String>,
364 ) -> Result<()> {
365 eyre::ensure!(crate_path.is_dir(), "Crate path must be a directory");
366
367 let cargo_toml_path = crate_path.join("Cargo.toml");
368
369 eyre::ensure!(cargo_toml_path.is_file(), "Cargo.toml must exist");
370 let cargo_toml_contents =
371 fs::read_to_string(cargo_toml_path).wrap_err("Failed to read Cargo.toml")?;
372
373 let name_check = format!("name = \"{name}\"");
374 let version_check = format!("version = \"{version}\"");
375 let alloy_dep_check = Self::get_alloy_dep(alloy_version, alloy_rev);
376 let serde_with_consistent =
377 !self.instances.iter().any(|instance| instance.needs_serde_with)
378 || cargo_toml_contents.contains(SERDE_WITH_DEP);
379 let toml_consistent = cargo_toml_contents.contains(&name_check)
380 && cargo_toml_contents.contains(&version_check)
381 && cargo_toml_contents.contains(&alloy_dep_check)
382 && serde_with_consistent;
383 eyre::ensure!(
384 toml_consistent,
385 r#"The contents of Cargo.toml do not match the expected output of the latest `sol!` version.
386 This indicates that the existing bindings are outdated and need to be generated again."#
387 );
388
389 Ok(())
390 }
391
392 fn get_alloy_dep(alloy_version: Option<String>, alloy_rev: Option<String>) -> String {
396 if let Some(alloy_version) = alloy_version {
397 format!(
398 r#"alloy = {{ version = "{alloy_version}", features = ["sol-types", "contract"] }}"#,
399 )
400 } else if let Some(alloy_rev) = alloy_rev {
401 format!(
402 r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{alloy_rev}", features = ["sol-types", "contract"] }}"#,
403 )
404 } else {
405 r#"alloy = { version = "1.0", features = ["sol-types", "contract"] }"#.to_string()
406 }
407 }
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}