Skip to main content

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, 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
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/// Extract both regular and `sol` derive attributes for propagation further.
135fn 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    // Ident::new panics on Rust keywords and `r#` prefixes
160    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}