forge/cmd/
bind_json.rs

1use super::eip712::Resolver;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::{
5    opts::{BuildOpts, solar_pcx_from_solc_project},
6    utils::LoadConfig,
7};
8use foundry_common::{TYPE_BINDING_PREFIX, fs};
9use foundry_compilers::{
10    CompilerInput, Graph, Project,
11    artifacts::{Source, Sources},
12    multi::{MultiCompilerLanguage, MultiCompilerParsedSource},
13    solc::{SolcLanguage, SolcVersionedInput},
14};
15use foundry_config::Config;
16use itertools::Itertools;
17use path_slash::PathExt;
18use rayon::prelude::*;
19use semver::Version;
20use solar_parse::{
21    Parser as SolarParser,
22    ast::{self, Arena, FunctionKind, Span, VarMut, interface::source_map::FileName, visit::Visit},
23    interface::Session,
24};
25use solar_sema::thread_local::ThreadLocal;
26use std::{
27    collections::{BTreeMap, BTreeSet, HashSet},
28    fmt::Write,
29    ops::ControlFlow,
30    path::{Path, PathBuf},
31    sync::Arc,
32};
33
34foundry_config::impl_figment_convert!(BindJsonArgs, build);
35
36const JSON_BINDINGS_PLACEHOLDER: &str = "library JsonBindings {}";
37
38/// CLI arguments for `forge bind-json`.
39#[derive(Clone, Debug, Parser)]
40pub struct BindJsonArgs {
41    /// The path to write bindings to.
42    #[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
43    pub out: Option<PathBuf>,
44
45    #[command(flatten)]
46    build: BuildOpts,
47}
48
49impl BindJsonArgs {
50    pub fn run(self) -> Result<()> {
51        let config = self.load_config()?;
52        let project = config.ephemeral_project()?;
53        let target_path = config.root.join(self.out.as_ref().unwrap_or(&config.bind_json.out));
54
55        // Step 1: Read and preprocess sources
56        let sources = project.paths.read_input_files()?;
57        let graph = Graph::<MultiCompilerParsedSource>::resolve_sources(&project.paths, sources)?;
58
59        // We only generate bindings for a single Solidity version to avoid conflicts.
60        let (version, mut sources, _) = graph
61            // resolve graph into mapping language -> version -> sources
62            .into_sources_by_version(&project)?
63            .sources
64            .into_iter()
65            // we are only interested in Solidity sources
66            .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
67            .ok_or_else(|| eyre::eyre!("no Solidity sources"))?
68            .1
69            .into_iter()
70            // For now, we are always picking the latest version.
71            .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
72            .unwrap();
73
74        // Step 2: Preprocess sources to handle potentially invalid bindings
75        self.preprocess_sources(&mut sources)?;
76
77        // Insert empty bindings file.
78        sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER));
79
80        // Step 3: Find structs and generate bindings
81        let structs_to_write =
82            self.find_and_resolve_structs(&config, &project, version, sources, &target_path)?;
83
84        // Step 4: Write bindings
85        self.write_bindings(&structs_to_write, &target_path)?;
86
87        Ok(())
88    }
89
90    /// In cases when user moves/renames/deletes structs, compiler will start failing because
91    /// generated bindings will be referencing non-existing structs or importing non-existing
92    /// files.
93    ///
94    /// Because of that, we need a little bit of preprocessing to make sure that bindings will still
95    /// be valid.
96    ///
97    /// The strategy is:
98    /// 1. Replace bindings file with an empty one to get rid of potentially invalid imports.
99    /// 2. Remove all function bodies to get rid of `serialize`/`deserialize` invocations.
100    /// 3. Remove all `immutable` attributes to avoid errors because of erased constructors
101    ///    initializing them.
102    ///
103    /// After that we'll still have enough information for bindings but compilation should succeed
104    /// in most of the cases.
105    fn preprocess_sources(&self, sources: &mut Sources) -> Result<()> {
106        let sess = Session::builder().with_stderr_emitter().build();
107        let result = sess.enter_parallel(|| -> solar_parse::interface::Result<()> {
108            sources.0.par_iter_mut().try_for_each(|(path, source)| {
109                let mut content = Arc::try_unwrap(std::mem::take(&mut source.content)).unwrap();
110
111                let arena = Arena::new();
112                let mut parser = SolarParser::from_source_code(
113                    &sess,
114                    &arena,
115                    FileName::Real(path.clone()),
116                    content.to_string(),
117                )?;
118                let ast = parser.parse_file().map_err(|e| e.emit())?;
119
120                let mut visitor = PreprocessorVisitor::new();
121                let _ = visitor.visit_source_unit(&ast);
122                visitor.update(&sess, &mut content);
123
124                source.content = Arc::new(content);
125                Ok(())
126            })
127        });
128        eyre::ensure!(result.is_ok(), "failed parsing");
129        Ok(())
130    }
131
132    /// Find structs, resolve conflicts, and prepare them for writing
133    fn find_and_resolve_structs(
134        &self,
135        config: &Config,
136        project: &Project,
137        version: Version,
138        sources: Sources,
139        _target_path: &Path,
140    ) -> Result<Vec<StructToWrite>> {
141        let settings = config.solc_settings()?;
142        let include = &config.bind_json.include;
143        let exclude = &config.bind_json.exclude;
144        let root = &config.root;
145
146        let input = SolcVersionedInput::build(sources, settings, SolcLanguage::Solidity, version);
147
148        let mut sess = Session::builder().with_stderr_emitter().build();
149        sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
150
151        let mut structs_to_write = Vec::new();
152
153        sess.enter_parallel(|| -> Result<()> {
154            // Set up the parsing context with the project paths, without adding the source files
155            let mut parsing_context = solar_pcx_from_solc_project(&sess, project, &input, false);
156
157            let mut target_files = HashSet::new();
158            for (path, source) in &input.input.sources {
159                if !include.is_empty() {
160                    if !include.iter().any(|matcher| matcher.is_match(path)) {
161                        continue;
162                    }
163                } else {
164                    // Exclude library files by default
165                    if project.paths.has_library_ancestor(path) {
166                        continue;
167                    }
168                }
169
170                if exclude.iter().any(|matcher| matcher.is_match(path)) {
171                    continue;
172                }
173
174                if let Ok(src_file) =
175                    sess.source_map().new_source_file(path.clone(), source.content.as_str())
176                {
177                    target_files.insert(src_file.stable_id);
178                    parsing_context.add_file(src_file);
179                }
180            }
181
182            // Parse and resolve
183            let hir_arena = ThreadLocal::new();
184            if let Ok(Some(gcx)) = parsing_context.parse_and_lower(&hir_arena) {
185                let hir = &gcx.get().hir;
186                let resolver = Resolver::new(gcx);
187                for id in resolver.struct_ids() {
188                    if let Some(schema) = resolver.resolve_struct_eip712(id) {
189                        let def = hir.strukt(id);
190                        let source = hir.source(def.source);
191
192                        if !target_files.contains(&source.file.stable_id) {
193                            continue;
194                        }
195
196                        if let FileName::Real(ref path) = source.file.name {
197                            structs_to_write.push(StructToWrite {
198                                name: def.name.as_str().into(),
199                                contract_name: def
200                                    .contract
201                                    .map(|id| hir.contract(id).name.as_str().into()),
202                                path: path
203                                    .strip_prefix(root)
204                                    .unwrap_or_else(|_| path)
205                                    .to_path_buf(),
206                                schema,
207                                // will be filled later
208                                import_alias: None,
209                                name_in_fns: String::new(),
210                            });
211                        }
212                    }
213                }
214            }
215            Ok(())
216        })?;
217
218        eyre::ensure!(sess.dcx.has_errors().is_ok(), "errors occurred");
219
220        // Resolve import aliases and function names
221        self.resolve_conflicts(&mut structs_to_write);
222
223        Ok(structs_to_write)
224    }
225
226    /// We manage 2 namespaces for JSON bindings:
227    ///   - Namespace of imported items. This includes imports of contracts containing structs and
228    ///     structs defined at the file level.
229    ///   - Namespace of struct names used in function names and schema_* variables.
230    ///
231    /// Both of those might contain conflicts, so we need to resolve them.
232    fn resolve_conflicts(&self, structs_to_write: &mut [StructToWrite]) {
233        // firstly, we resolve imported names conflicts
234        // construct mapping name -> paths from which items with such name are imported
235        let mut names_to_paths = BTreeMap::new();
236
237        for s in structs_to_write.iter() {
238            names_to_paths
239                .entry(s.struct_or_contract_name())
240                .or_insert_with(BTreeSet::new)
241                .insert(s.path.as_path());
242        }
243
244        // now resolve aliases for names which need them and construct mapping (name, file) -> alias
245        let mut aliases = BTreeMap::new();
246
247        for (name, paths) in names_to_paths {
248            if paths.len() <= 1 {
249                continue; // no alias needed
250            }
251
252            for (i, path) in paths.into_iter().enumerate() {
253                aliases
254                    .entry(name.to_string())
255                    .or_insert_with(BTreeMap::new)
256                    .insert(path.to_path_buf(), format!("{name}_{i}"));
257            }
258        }
259
260        for s in structs_to_write.iter_mut() {
261            let name = s.struct_or_contract_name();
262            if aliases.contains_key(name) {
263                s.import_alias = Some(aliases[name][&s.path].clone());
264            }
265        }
266
267        // Each struct needs a name by which we are referencing it in function names (e.g.
268        // deserializeFoo) Those might also have conflicts, so we manage a separate
269        // namespace for them
270        let mut name_to_structs_indexes = BTreeMap::new();
271
272        for (idx, s) in structs_to_write.iter().enumerate() {
273            name_to_structs_indexes.entry(&s.name).or_insert_with(Vec::new).push(idx);
274        }
275
276        // Keeps `Some` for structs that will be referenced by name other than their definition
277        // name.
278        let mut fn_names = vec![None; structs_to_write.len()];
279
280        for (name, indexes) in name_to_structs_indexes {
281            if indexes.len() > 1 {
282                for (i, idx) in indexes.into_iter().enumerate() {
283                    fn_names[idx] = Some(format!("{name}_{i}"));
284                }
285            }
286        }
287
288        for (s, fn_name) in structs_to_write.iter_mut().zip(fn_names.into_iter()) {
289            s.name_in_fns = fn_name.unwrap_or(s.name.clone());
290        }
291    }
292
293    /// Write the final bindings file
294    fn write_bindings(
295        &self,
296        structs_to_write: &[StructToWrite],
297        target_path: &PathBuf,
298    ) -> Result<()> {
299        let mut result = String::new();
300
301        // Write imports
302        let mut grouped_imports = BTreeMap::new();
303        for struct_to_write in structs_to_write {
304            let item = struct_to_write.import_item();
305            grouped_imports
306                .entry(struct_to_write.path.as_path())
307                .or_insert_with(BTreeSet::new)
308                .insert(item);
309        }
310
311        result.push_str("// Automatically generated by forge bind-json.\n\npragma solidity >=0.6.2 <0.9.0;\npragma experimental ABIEncoderV2;\n\n");
312
313        for (path, names) in grouped_imports {
314            writeln!(
315                &mut result,
316                "import {{{}}} from \"{}\";",
317                names.iter().join(", "),
318                path.to_slash_lossy()
319            )?;
320        }
321
322        // Write VM interface
323        // Writes minimal VM interface to not depend on forge-std version
324        result.push_str(r#"
325interface Vm {
326    function parseJsonTypeArray(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory);
327    function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory);
328    function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory);
329    function serializeJsonType(string calldata typeDescription, bytes memory value) external pure returns (string memory json);
330    function serializeJsonType(string calldata objectKey, string calldata valueKey, string calldata typeDescription, bytes memory value) external returns (string memory json);
331}
332        "#);
333
334        // Write library
335        result.push_str(
336            r#"
337library JsonBindings {
338    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
339
340"#,
341        );
342
343        // write schema constants
344        for struct_to_write in structs_to_write {
345            writeln!(
346                &mut result,
347                "    {}{} = \"{}\";",
348                TYPE_BINDING_PREFIX, struct_to_write.name_in_fns, struct_to_write.schema
349            )?;
350        }
351
352        // write serialization functions
353        for struct_to_write in structs_to_write {
354            write!(
355                &mut result,
356                r#"
357    function serialize({path} memory value) internal pure returns (string memory) {{
358        return vm.serializeJsonType(schema_{name_in_fns}, abi.encode(value));
359    }}
360
361    function serialize({path} memory value, string memory objectKey, string memory valueKey) internal returns (string memory) {{
362        return vm.serializeJsonType(objectKey, valueKey, schema_{name_in_fns}, abi.encode(value));
363    }}
364
365    function deserialize{name_in_fns}(string memory json) public pure returns ({path} memory) {{
366        return abi.decode(vm.parseJsonType(json, schema_{name_in_fns}), ({path}));
367    }}
368
369    function deserialize{name_in_fns}(string memory json, string memory path) public pure returns ({path} memory) {{
370        return abi.decode(vm.parseJsonType(json, path, schema_{name_in_fns}), ({path}));
371    }}
372
373    function deserialize{name_in_fns}Array(string memory json, string memory path) public pure returns ({path}[] memory) {{
374        return abi.decode(vm.parseJsonTypeArray(json, path, schema_{name_in_fns}), ({path}[]));
375    }}
376"#,
377                name_in_fns = struct_to_write.name_in_fns,
378                path = struct_to_write.full_path()
379            )?;
380        }
381
382        result.push_str("}\n");
383
384        // Write to file
385        if let Some(parent) = target_path.parent() {
386            fs::create_dir_all(parent)?;
387        }
388        fs::write(target_path, &result)?;
389
390        sh_println!("Bindings written to {}", target_path.display())?;
391
392        Ok(())
393    }
394}
395
396struct PreprocessorVisitor {
397    updates: Vec<(Span, &'static str)>,
398}
399
400impl PreprocessorVisitor {
401    fn new() -> Self {
402        Self { updates: Vec::new() }
403    }
404
405    fn update(mut self, sess: &Session, content: &mut String) {
406        if self.updates.is_empty() {
407            return;
408        }
409
410        let sf = sess.source_map().lookup_source_file(self.updates[0].0.lo());
411        let base = sf.start_pos.0;
412
413        self.updates.sort_by_key(|(span, _)| span.lo());
414        let mut shift = 0_i64;
415        for (span, new) in self.updates {
416            let lo = span.lo() - base;
417            let hi = span.hi() - base;
418            let start = ((lo.0 as i64) - shift) as usize;
419            let end = ((hi.0 as i64) - shift) as usize;
420
421            content.replace_range(start..end, new);
422            shift += (end - start) as i64;
423            shift -= new.len() as i64;
424        }
425    }
426}
427
428impl<'ast> Visit<'ast> for PreprocessorVisitor {
429    type BreakValue = solar_parse::interface::data_structures::Never;
430
431    fn visit_item_function(
432        &mut self,
433        func: &'ast ast::ItemFunction<'ast>,
434    ) -> ControlFlow<Self::BreakValue> {
435        // Replace function bodies with a noop statement.
436        if let Some(block) = &func.body
437            && !block.is_empty()
438        {
439            let span = block.first().unwrap().span.to(block.last().unwrap().span);
440            let new_body = match func.kind {
441                FunctionKind::Modifier => "_;",
442                _ => "revert();",
443            };
444            self.updates.push((span, new_body));
445        }
446
447        self.walk_item_function(func)
448    }
449
450    fn visit_variable_definition(
451        &mut self,
452        var: &'ast ast::VariableDefinition<'ast>,
453    ) -> ControlFlow<Self::BreakValue> {
454        // Remove `immutable` attributes.
455        if let Some(VarMut::Immutable) = var.mutability {
456            self.updates.push((var.span, ""));
457        }
458
459        self.walk_variable_definition(var)
460    }
461}
462
463/// A single struct definition for which we need to generate bindings.
464#[derive(Debug, Clone)]
465struct StructToWrite {
466    /// Name of the struct definition.
467    name: String,
468    /// Name of the contract containing the struct definition. None if the struct is defined at the
469    /// file level.
470    contract_name: Option<String>,
471    /// Import alias for the contract or struct, depending on whether the struct is imported
472    /// directly, or via a contract.
473    import_alias: Option<String>,
474    /// Path to the file containing the struct definition.
475    path: PathBuf,
476    /// EIP712 schema for the struct.
477    schema: String,
478    /// Name of the struct definition used in function names and schema_* variables.
479    name_in_fns: String,
480}
481
482impl StructToWrite {
483    /// Returns the name of the imported item. If struct is defined at the file level, returns the
484    /// struct name, otherwise returns the parent contract name.
485    fn struct_or_contract_name(&self) -> &str {
486        self.contract_name.as_deref().unwrap_or(&self.name)
487    }
488
489    /// Same as [StructToWrite::struct_or_contract_name] but with alias applied.
490    fn struct_or_contract_name_with_alias(&self) -> &str {
491        self.import_alias.as_deref().unwrap_or(self.struct_or_contract_name())
492    }
493
494    /// Path which can be used to reference this struct in input/output parameters. Either
495    /// StructName or ParentName.StructName
496    fn full_path(&self) -> String {
497        if self.contract_name.is_some() {
498            format!("{}.{}", self.struct_or_contract_name_with_alias(), self.name)
499        } else {
500            self.struct_or_contract_name_with_alias().to_string()
501        }
502    }
503
504    fn import_item(&self) -> String {
505        if let Some(alias) = &self.import_alias {
506            format!("{} as {}", self.struct_or_contract_name(), alias)
507        } else {
508            self.struct_or_contract_name().to_string()
509        }
510    }
511}