1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
use crate::{SolInput, SolInputKind};
use alloy_json_abi::{ContractObject, JsonAbi, ToSolConfig};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::quote;
use syn::{AttrStyle, Result};

impl SolInput {
    /// Normalize JSON ABI inputs into Sol inputs.
    pub fn normalize_json(self) -> Result<Self> {
        let SolInput {
            attrs,
            path,
            kind: SolInputKind::Json(name, ContractObject { abi, bytecode, deployed_bytecode }),
        } = self
        else {
            return Ok(self);
        };

        let mut abi = abi.ok_or_else(|| syn::Error::new(name.span(), "ABI not found in JSON"))?;
        let sol = abi_to_sol(&name, &mut abi);
        let mut all_tokens = tokens_for_sol(&name, &sol)?.into_iter();

        let (inner_attrs, attrs) = attrs
            .into_iter()
            .partition::<Vec<_>, _>(|attr| matches!(attr.style, AttrStyle::Inner(_)));

        let derives =
            attrs.iter().filter(|attr| attr.path().is_ident("derive")).collect::<Vec<_>>();

        let mut library_tokens_iter = all_tokens
            .by_ref()
            .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "interface"))
            .skip_while(|tt| matches!(tt, TokenTree::Ident(id) if id == "library"))
            .peekable();

        let library_tokens = library_tokens_iter.by_ref();

        let mut libraries = Vec::new();

        while library_tokens.peek().is_some() {
            let sol_library_tokens: TokenStream = std::iter::once(TokenTree::Ident(id("library")))
                .chain(
                    library_tokens
                        .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "library")),
                )
                .collect();

            let tokens = quote! {
                #(#derives)*
                #sol_library_tokens
            };

            libraries.push(tokens);
        }
        let sol_interface_tokens: TokenStream =
            std::iter::once(TokenTree::Ident(id("interface"))).chain(all_tokens).collect();
        let bytecode = bytecode.map(|bytes| {
            let s = bytes.to_string();
            quote!(bytecode = #s,)
        });
        let deployed_bytecode = deployed_bytecode.map(|bytes| {
            let s = bytes.to_string();
            quote!(deployed_bytecode = #s)
        });

        let attrs_iter = attrs.iter();
        let doc_str = format!(
            "\n\n\
Generated by the following Solidity interface...
```solidity
{sol}
```

...which was generated by the following JSON ABI:
```json
{json_s}
```",
            json_s = serde_json::to_string_pretty(&abi).unwrap()
        );
        let tokens = quote! {
            #(#inner_attrs)*
            #(#libraries)*

            #(#attrs_iter)*
            #[doc = #doc_str]
            #[sol(#bytecode #deployed_bytecode)]
            #sol_interface_tokens
        };

        let ast: ast::File = syn::parse2(tokens).map_err(|e| {
            let msg = format!(
                "failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\
                 This is a bug. We would appreciate a bug report: \
                 https://github.com/alloy-rs/core/issues/new/choose"
            );
            syn::Error::new(name.span(), msg)
        })?;

        let kind = SolInputKind::Sol(ast);
        Ok(SolInput { attrs, path, kind })
    }
}

// doesn't parse Json

fn abi_to_sol(name: &Ident, abi: &mut JsonAbi) -> String {
    abi.dedup();
    let config = ToSolConfig::new().print_constructors(true).for_sol_macro(true);
    abi.to_sol(&name.to_string(), Some(config))
}

/// Returns `sol!` tokens.
pub fn tokens_for_sol(name: &Ident, sol: &str) -> Result<TokenStream> {
    let mk_err = |s: &str| {
        let msg = format!(
            "`JsonAbi::to_sol` generated invalid Rust tokens for `{name}`: {s}\n\
             This is a bug. We would appreciate a bug report: \
             https://github.com/alloy-rs/core/issues/new/choose"
        );
        syn::Error::new(name.span(), msg)
    };
    let tts = syn::parse_str::<TokenStream>(sol).map_err(|e| mk_err(&e.to_string()))?;
    Ok(tts
        .into_iter()
        .map(|mut tt| {
            if matches!(&tt, TokenTree::Ident(id) if id == name) {
                tt.set_span(name.span());
            }
            tt
        })
        .collect())
}

#[inline]
#[track_caller]
fn id(s: impl AsRef<str>) -> Ident {
    // Ident::new panics on Rust keywords and `r#` prefixes
    syn::parse_str(s.as_ref()).unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};

    #[test]
    #[cfg_attr(miri, ignore = "no fs")]
    fn abi() {
        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../json-abi/tests/abi");
        for file in std::fs::read_dir(path).unwrap() {
            let path = file.unwrap().path();
            if path.extension() != Some("json".as_ref()) {
                continue;
            }

            if path.file_name() == Some("LargeFunction.json".as_ref()) {
                continue;
            }
            parse_test(&std::fs::read_to_string(&path).unwrap(), path.to_str().unwrap());
        }
    }

    fn parse_test(s: &str, path: &str) {
        let mut abi: JsonAbi = serde_json::from_str(s).unwrap();
        let name = Path::new(path).file_stem().unwrap().to_str().unwrap();

        let name_id = id(name);
        let sol = abi_to_sol(&name_id, &mut abi);
        let tokens = match tokens_for_sol(&name_id, &sol) {
            Ok(tokens) => tokens,
            Err(e) => {
                let path = write_tmp_sol(name, &sol);
                panic!(
                    "couldn't expand JSON ABI for {name:?}: {e}\n\
                     emitted interface: {}",
                    path.display()
                );
            }
        };

        let _ast = match syn::parse2::<ast::File>(tokens.clone()) {
            Ok(ast) => ast,
            Err(e) => {
                let spath = write_tmp_sol(name, &sol);
                let tpath = write_tmp_sol(&format!("{name}.tokens"), &tokens.to_string());
                panic!(
                    "couldn't parse expanded JSON ABI back to AST for {name:?}: {e}\n\
                     emitted interface: {}\n\
                     emitted tokens:    {}",
                    spath.display(),
                    tpath.display(),
                );
            }
        };
    }

    fn write_tmp_sol(name: &str, contents: &str) -> PathBuf {
        let path = std::env::temp_dir().join(format!("sol-macro-{name}.sol"));
        std::fs::write(&path, contents).unwrap();
        let _ = std::process::Command::new("forge").arg("fmt").arg(&path).output();
        path
    }
}