forge_sol_macro_gen/
sol_macro_gen.rs

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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
//! SolMacroGen and MultiSolMacroGen
//!
//! This type encapsulates the logic for expansion of a Rust TokenStream from Solidity tokens. It
//! uses the `expand` method from `alloy_sol_macro_expander` underneath.
//!
//! It holds info such as `path` to the ABI file, `name` of the file and the rust binding being
//! generated, and lastly the `expansion` itself, i.e the Rust binding for the provided ABI.
//!
//! It contains methods to read the json abi, generate rust bindings from the abi and ultimately
//! write the bindings to a crate or modules.

use alloy_sol_macro_expander::expand::expand;
use alloy_sol_macro_input::{SolInput, SolInputKind};
use eyre::{Context, OptionExt, Result};
use foundry_common::fs;
use proc_macro2::{Span, TokenStream};
use std::{
    fmt::Write,
    path::{Path, PathBuf},
    str::FromStr,
};

pub struct SolMacroGen {
    pub path: PathBuf,
    pub name: String,
    pub expansion: Option<TokenStream>,
}

impl SolMacroGen {
    pub fn new(path: PathBuf, name: String) -> Self {
        Self { path, name, expansion: None }
    }

    pub fn get_sol_input(&self) -> Result<SolInput> {
        let path = self.path.to_string_lossy().into_owned();
        let name = proc_macro2::Ident::new(&self.name, Span::call_site());
        let tokens = quote::quote! {
            #name,
            #path
        };

        let sol_input: SolInput = syn::parse2(tokens).wrap_err("failed to parse input")?;

        Ok(sol_input)
    }
}

pub struct MultiSolMacroGen {
    pub artifacts_path: PathBuf,
    pub instances: Vec<SolMacroGen>,
}

impl MultiSolMacroGen {
    pub fn new(artifacts_path: &Path, instances: Vec<SolMacroGen>) -> Self {
        Self { artifacts_path: artifacts_path.to_path_buf(), instances }
    }

    pub fn populate_expansion(&mut self, bindings_path: &Path) -> Result<()> {
        for instance in &mut self.instances {
            let path = bindings_path.join(format!("{}.rs", instance.name.to_lowercase()));
            let expansion = fs::read_to_string(path).wrap_err("Failed to read file")?;

            let tokens = TokenStream::from_str(&expansion)
                .map_err(|e| eyre::eyre!("Failed to parse TokenStream: {e}"))?;
            instance.expansion = Some(tokens);
        }
        Ok(())
    }

    pub fn generate_bindings(&mut self) -> Result<()> {
        for instance in &mut self.instances {
            Self::generate_binding(instance).wrap_err_with(|| {
                format!(
                    "failed to generate bindings for {}:{}",
                    instance.path.display(),
                    instance.name
                )
            })?;
        }

        Ok(())
    }

    fn generate_binding(instance: &mut SolMacroGen) -> Result<()> {
        let input = instance.get_sol_input()?.normalize_json()?;

        let SolInput { attrs: _, path: _, kind } = input;

        let tokens = match kind {
            SolInputKind::Sol(mut file) => {
                let sol_attr: syn::Attribute = syn::parse_quote! {
                    #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract = alloy::contract)]
                };
                file.attrs.push(sol_attr);
                expand(file).wrap_err("failed to expand")?
            }
            _ => unreachable!(),
        };

        instance.expansion = Some(tokens);
        Ok(())
    }

    pub fn write_to_crate(
        &mut self,
        name: &str,
        version: &str,
        bindings_path: &Path,
        single_file: bool,
        alloy_version: Option<String>,
    ) -> Result<()> {
        self.generate_bindings()?;

        let src = bindings_path.join("src");

        let _ = fs::create_dir_all(&src);

        // Write Cargo.toml
        let cargo_toml_path = bindings_path.join("Cargo.toml");
        let mut toml_contents = format!(
            r#"[package]
name = "{name}"
version = "{version}"
edition = "2021"

[dependencies]
"#
        );

        let alloy_dep = if let Some(alloy_version) = alloy_version {
            format!(
                r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{alloy_version}", features = ["sol-types", "contract"] }}"#
            )
        } else {
            r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
        };
        write!(toml_contents, "{alloy_dep}")?;

        fs::write(cargo_toml_path, toml_contents).wrap_err("Failed to write Cargo.toml")?;

        let mut lib_contents = String::new();
        write!(
            &mut lib_contents,
            r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
        //! This module contains the sol! generated bindings for solidity contracts.
        //! This is autogenerated code.
        //! Do not manually edit these files.
        //! These files may be overwritten by the codegen system at any time.
        "#
        )?;

        // Write src
        let parse_error = |name: &str| {
            format!("failed to parse generated tokens as an AST for {name};\nthis is likely a bug")
        };
        for instance in &self.instances {
            let contents = instance.expansion.as_ref().unwrap();

            let name = instance.name.to_lowercase();
            let path = src.join(format!("{name}.rs"));
            let file = syn::parse2(contents.clone())
                .wrap_err_with(|| parse_error(&format!("{}:{}", path.display(), name)))?;
            let contents = prettyplease::unparse(&file);
            if single_file {
                write!(&mut lib_contents, "{contents}")?;
            } else {
                fs::write(path, contents).wrap_err("failed to write to file")?;
                writeln!(&mut lib_contents, "pub mod {name};")?;
            }
        }

        let lib_path = src.join("lib.rs");
        let lib_file = syn::parse_file(&lib_contents).wrap_err_with(|| parse_error("lib.rs"))?;
        let lib_contents = prettyplease::unparse(&lib_file);
        fs::write(lib_path, lib_contents).wrap_err("Failed to write lib.rs")?;

        Ok(())
    }

    pub fn write_to_module(&mut self, bindings_path: &Path, single_file: bool) -> Result<()> {
        self.generate_bindings()?;

        let _ = fs::create_dir_all(bindings_path);

        let mut mod_contents = r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
        //! This module contains the sol! generated bindings for solidity contracts.
        //! This is autogenerated code.
        //! Do not manually edit these files.
        //! These files may be overwritten by the codegen system at any time.
        "#
        .to_string();

        for instance in &self.instances {
            let name = instance.name.to_lowercase();
            if !single_file {
                // Module
                write!(
                    mod_contents,
                    r#"pub mod {};
                "#,
                    instance.name.to_lowercase()
                )?;
                let mut contents = String::new();

                write!(contents, "{}", instance.expansion.as_ref().unwrap())?;
                let file = syn::parse_file(&contents)?;

                let contents = prettyplease::unparse(&file);
                fs::write(bindings_path.join(format!("{name}.rs")), contents)
                    .wrap_err("Failed to write file")?;
            } else {
                // Single File
                let mut contents = String::new();
                write!(contents, "{}\n\n", instance.expansion.as_ref().unwrap())?;
                write!(mod_contents, "{contents}")?;
            }
        }

        let mod_path = bindings_path.join("mod.rs");
        let mod_file = syn::parse_file(&mod_contents)?;
        let mod_contents = prettyplease::unparse(&mod_file);

        fs::write(mod_path, mod_contents).wrap_err("Failed to write mod.rs")?;

        Ok(())
    }

    /// Checks that the generated bindings are up to date with the latest version of
    /// `sol!`.
    ///
    /// Returns `Ok(())` if the generated bindings are up to date, otherwise it returns
    /// `Err(_)`.
    #[allow(clippy::too_many_arguments)]
    pub fn check_consistency(
        &self,
        name: &str,
        version: &str,
        crate_path: &Path,
        single_file: bool,
        check_cargo_toml: bool,
        is_mod: bool,
        alloy_version: Option<String>,
    ) -> Result<()> {
        if check_cargo_toml {
            self.check_cargo_toml(name, version, crate_path, alloy_version)?;
        }

        let mut super_contents = String::new();
        write!(
            &mut super_contents,
            r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
            //! This module contains the sol! generated bindings for solidity contracts.
            //! This is autogenerated code.
            //! Do not manually edit these files.
            //! These files may be overwritten by the codegen system at any time.
            "#
        )?;
        if !single_file {
            for instance in &self.instances {
                let name = instance.name.to_lowercase();
                let path = if is_mod {
                    crate_path.join(format!("{name}.rs"))
                } else {
                    crate_path.join(format!("src/{name}.rs"))
                };
                let tokens = instance
                    .expansion
                    .as_ref()
                    .ok_or_eyre(format!("TokenStream for {path:?} does not exist"))?
                    .to_string();

                self.check_file_contents(&path, &tokens)?;

                write!(
                    &mut super_contents,
                    r#"pub mod {name};
                    "#
                )?;
            }

            let super_path =
                if is_mod { crate_path.join("mod.rs") } else { crate_path.join("src/lib.rs") };
            self.check_file_contents(&super_path, &super_contents)?;
        }

        Ok(())
    }

    fn check_file_contents(&self, file_path: &Path, expected_contents: &str) -> Result<()> {
        eyre::ensure!(
            file_path.is_file() && file_path.exists(),
            "{} is not a file",
            file_path.display()
        );
        let file_contents = &fs::read_to_string(file_path).wrap_err("Failed to read file")?;

        // Format both
        let file_contents = syn::parse_file(file_contents)?;
        let formatted_file = prettyplease::unparse(&file_contents);

        let expected_contents = syn::parse_file(expected_contents)?;
        let formatted_exp = prettyplease::unparse(&expected_contents);

        eyre::ensure!(
            formatted_file == formatted_exp,
            "File contents do not match expected contents for {file_path:?}"
        );
        Ok(())
    }

    fn check_cargo_toml(
        &self,
        name: &str,
        version: &str,
        crate_path: &Path,
        alloy_version: Option<String>,
    ) -> Result<()> {
        eyre::ensure!(crate_path.is_dir(), "Crate path must be a directory");

        let cargo_toml_path = crate_path.join("Cargo.toml");

        eyre::ensure!(cargo_toml_path.is_file(), "Cargo.toml must exist");
        let cargo_toml_contents =
            fs::read_to_string(cargo_toml_path).wrap_err("Failed to read Cargo.toml")?;

        let name_check = format!("name = \"{name}\"");
        let version_check = format!("version = \"{version}\"");
        let alloy_dep_check = if let Some(version) = alloy_version {
            format!(
                r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{version}", features = ["sol-types", "contract"] }}"#,
            )
        } else {
            r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
        };
        let toml_consistent = cargo_toml_contents.contains(&name_check) &&
            cargo_toml_contents.contains(&version_check) &&
            cargo_toml_contents.contains(&alloy_dep_check);
        eyre::ensure!(
            toml_consistent,
            r#"The contents of Cargo.toml do not match the expected output of the latest `sol!` version.
                This indicates that the existing bindings are outdated and need to be generated again."#
        );

        Ok(())
    }
}