alloy_sol_macro_input/
json.rs1use crate::{SolInput, SolInputKind};
2use alloy_json_abi::{ContractObject, JsonAbi, ToSolConfig};
3use proc_macro2::{Ident, TokenStream, TokenTree};
4use quote::quote;
5use syn::{AttrStyle, Result};
6
7impl SolInput {
8 pub fn normalize_json(self) -> Result<Self> {
10 let SolInput {
11 attrs,
12 path,
13 kind: SolInputKind::Json(name, ContractObject { abi, bytecode, deployed_bytecode }),
14 } = self
15 else {
16 return Ok(self);
17 };
18
19 let mut abi = abi.ok_or_else(|| syn::Error::new(name.span(), "ABI not found in JSON"))?;
20 let sol = abi_to_sol(&name, &mut abi);
21 let mut all_tokens = tokens_for_sol(&name, &sol)?.into_iter();
22
23 let (inner_attrs, attrs) = attrs
24 .into_iter()
25 .partition::<Vec<_>, _>(|attr| matches!(attr.style, AttrStyle::Inner(_)));
26
27 let (derives, sol_derives) = extract_derive_attrs(&attrs);
28
29 let mut library_tokens_iter = all_tokens
30 .by_ref()
31 .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "interface"))
32 .skip_while(|tt| matches!(tt, TokenTree::Ident(id) if id == "library"))
33 .peekable();
34
35 let library_tokens = library_tokens_iter.by_ref();
36
37 let mut libraries = Vec::new();
38
39 while library_tokens.peek().is_some() {
40 let sol_library_tokens: TokenStream = std::iter::once(TokenTree::Ident(id("library")))
41 .chain(
42 library_tokens
43 .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "library")),
44 )
45 .collect();
46
47 let tokens = quote! {
48 #(#derives)*
49 #(#sol_derives)*
50 #sol_library_tokens
51 };
52
53 libraries.push(tokens);
54 }
55 let sol_interface_tokens: TokenStream =
56 std::iter::once(TokenTree::Ident(id("interface"))).chain(all_tokens).collect();
57 let bytecode = bytecode.map(|bytes| {
58 let s = bytes.to_string();
59 quote!(bytecode = #s,)
60 });
61 let deployed_bytecode = deployed_bytecode.map(|bytes| {
62 let s = bytes.to_string();
63 quote!(deployed_bytecode = #s)
64 });
65
66 let attrs_iter = attrs.iter();
67 let doc_str = format!(
68 "\n\n\
69Generated by the following Solidity interface...
70```solidity
71{sol}
72```
73
74...which was generated by the following JSON ABI:
75```json
76{json_s}
77```",
78 json_s = serde_json::to_string_pretty(&abi).unwrap()
79 );
80 let tokens = quote! {
81 #(#inner_attrs)*
82 #(#libraries)*
83
84 #(#attrs_iter)*
85 #[doc = #doc_str]
86 #[sol(#bytecode #deployed_bytecode)]
87 #sol_interface_tokens
88 };
89
90 let ast: ast::File = syn::parse2(tokens).map_err(|e| {
91 let msg = format!(
92 "failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\
93 This is a bug. We would appreciate a bug report: \
94 https://github.com/alloy-rs/core/issues/new/choose"
95 );
96 syn::Error::new(name.span(), msg)
97 })?;
98
99 let kind = SolInputKind::Sol(ast);
100 Ok(SolInput { attrs, path, kind })
101 }
102}
103
104fn abi_to_sol(name: &Ident, abi: &mut JsonAbi) -> String {
107 abi.dedup();
108 let config = ToSolConfig::new().print_constructors(true).for_sol_macro(true);
109 abi.to_sol(&name.to_string(), Some(config))
110}
111
112pub fn tokens_for_sol(name: &Ident, sol: &str) -> Result<TokenStream> {
114 let mk_err = |s: &str| {
115 let msg = format!(
116 "`JsonAbi::to_sol` generated invalid Rust tokens for `{name}`: {s}\n\
117 This is a bug. We would appreciate a bug report: \
118 https://github.com/alloy-rs/core/issues/new/choose"
119 );
120 syn::Error::new(name.span(), msg)
121 };
122 let tts = syn::parse_str::<TokenStream>(sol).map_err(|e| mk_err(&e.to_string()))?;
123 Ok(tts
124 .into_iter()
125 .map(|mut tt| {
126 if matches!(&tt, TokenTree::Ident(id) if id == name) {
127 tt.set_span(name.span());
128 }
129 tt
130 })
131 .collect())
132}
133
134fn extract_derive_attrs(attrs: &[syn::Attribute]) -> (Vec<&syn::Attribute>, Vec<&syn::Attribute>) {
136 attrs.iter().fold((Vec::new(), Vec::new()), |(mut derives, mut sol_derives), attr| {
137 if attr.path().is_ident("derive") {
138 derives.push(attr);
139 } else if attr.path().is_ident("sol") {
140 if let Ok(meta) = attr.meta.require_list() {
141 let mut contains_derives = false;
142 let _ = meta.parse_nested_meta(|meta| {
143 contains_derives |=
144 meta.path.is_ident("all_derives") || meta.path.is_ident("extra_derives");
145 Ok(())
146 });
147 if contains_derives {
148 sol_derives.push(attr);
149 }
150 }
151 }
152 (derives, sol_derives)
153 })
154}
155
156#[inline]
157#[track_caller]
158fn id(s: impl AsRef<str>) -> Ident {
159 syn::parse_str(s.as_ref()).unwrap()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use std::path::{Path, PathBuf};
167
168 #[test]
169 #[cfg_attr(miri, ignore = "no fs")]
170 fn abi() {
171 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../json-abi/tests/abi");
172 for file in std::fs::read_dir(path).unwrap() {
173 let path = file.unwrap().path();
174 if path.extension() != Some("json".as_ref()) {
175 continue;
176 }
177
178 if path.file_name() == Some("LargeFunction.json".as_ref())
179 || path.file_name() == Some("SomeLibUser.json".as_ref())
180 {
181 continue;
182 }
183 parse_test(&std::fs::read_to_string(&path).unwrap(), path.to_str().unwrap());
184 }
185 }
186
187 fn parse_test(s: &str, path: &str) {
188 let mut abi: JsonAbi = serde_json::from_str(s).unwrap();
189 let name = Path::new(path).file_stem().unwrap().to_str().unwrap();
190
191 let name_id = id(name);
192 let sol = abi_to_sol(&name_id, &mut abi);
193 let tokens = match tokens_for_sol(&name_id, &sol) {
194 Ok(tokens) => tokens,
195 Err(e) => {
196 let path = write_tmp_sol(name, &sol);
197 panic!(
198 "couldn't expand JSON ABI for {name:?}: {e}\n\
199 emitted interface: {}",
200 path.display()
201 );
202 }
203 };
204
205 let _ast = match syn::parse2::<ast::File>(tokens.clone()) {
206 Ok(ast) => ast,
207 Err(e) => {
208 let spath = write_tmp_sol(name, &sol);
209 let tpath = write_tmp_sol(&format!("{name}.tokens"), &tokens.to_string());
210 panic!(
211 "couldn't parse expanded JSON ABI back to AST for {name:?}: {e}\n\
212 emitted interface: {}\n\
213 emitted tokens: {}",
214 spath.display(),
215 tpath.display(),
216 );
217 }
218 };
219 }
220
221 fn write_tmp_sol(name: &str, contents: &str) -> PathBuf {
222 let path = std::env::temp_dir().join(format!("sol-macro-{name}.sol"));
223 std::fs::write(&path, contents).unwrap();
224 let _ = std::process::Command::new("forge").arg("fmt").arg(&path).output();
225 path
226 }
227}