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    /// Normalize JSON ABI inputs into Sol inputs.
9    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
104// doesn't parse Json
105
106fn 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
112/// Returns `sol!` tokens.
113pub 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    // Ident::new panics on Rust keywords and `r#` prefixes
138    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}