alloy_sol_macro_input/
json.rs
1use 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 =
28 attrs.iter().filter(|attr| attr.path().is_ident("derive")).collect::<Vec<_>>();
29
30 let mut library_tokens_iter = all_tokens
31 .by_ref()
32 .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "interface"))
33 .skip_while(|tt| matches!(tt, TokenTree::Ident(id) if id == "library"))
34 .peekable();
35
36 let library_tokens = library_tokens_iter.by_ref();
37
38 let mut libraries = Vec::new();
39
40 while library_tokens.peek().is_some() {
41 let sol_library_tokens: TokenStream = std::iter::once(TokenTree::Ident(id("library")))
42 .chain(
43 library_tokens
44 .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "library")),
45 )
46 .collect();
47
48 let tokens = quote! {
49 #(#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
134#[inline]
135#[track_caller]
136fn id(s: impl AsRef<str>) -> Ident {
137 syn::parse_str(s.as_ref()).unwrap()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::path::{Path, PathBuf};
145
146 #[test]
147 #[cfg_attr(miri, ignore = "no fs")]
148 fn abi() {
149 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../json-abi/tests/abi");
150 for file in std::fs::read_dir(path).unwrap() {
151 let path = file.unwrap().path();
152 if path.extension() != Some("json".as_ref()) {
153 continue;
154 }
155
156 if path.file_name() == Some("LargeFunction.json".as_ref()) {
157 continue;
158 }
159 parse_test(&std::fs::read_to_string(&path).unwrap(), path.to_str().unwrap());
160 }
161 }
162
163 fn parse_test(s: &str, path: &str) {
164 let mut abi: JsonAbi = serde_json::from_str(s).unwrap();
165 let name = Path::new(path).file_stem().unwrap().to_str().unwrap();
166
167 let name_id = id(name);
168 let sol = abi_to_sol(&name_id, &mut abi);
169 let tokens = match tokens_for_sol(&name_id, &sol) {
170 Ok(tokens) => tokens,
171 Err(e) => {
172 let path = write_tmp_sol(name, &sol);
173 panic!(
174 "couldn't expand JSON ABI for {name:?}: {e}\n\
175 emitted interface: {}",
176 path.display()
177 );
178 }
179 };
180
181 let _ast = match syn::parse2::<ast::File>(tokens.clone()) {
182 Ok(ast) => ast,
183 Err(e) => {
184 let spath = write_tmp_sol(name, &sol);
185 let tpath = write_tmp_sol(&format!("{name}.tokens"), &tokens.to_string());
186 panic!(
187 "couldn't parse expanded JSON ABI back to AST for {name:?}: {e}\n\
188 emitted interface: {}\n\
189 emitted tokens: {}",
190 spath.display(),
191 tpath.display(),
192 );
193 }
194 };
195 }
196
197 fn write_tmp_sol(name: &str, contents: &str) -> PathBuf {
198 let path = std::env::temp_dir().join(format!("sol-macro-{name}.sol"));
199 std::fs::write(&path, contents).unwrap();
200 let _ = std::process::Command::new("forge").arg("fmt").arg(&path).output();
201 path
202 }
203}