Skip to main content

forge/cmd/
bind_json.rs

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