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#[derive(Clone, Debug, Parser)]
39pub struct BindJsonArgs {
40 #[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 let sources = project.paths.read_input_files()?;
56 let graph = Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?;
57
58 let (version, mut sources, _) = graph
60 .into_sources_by_version(&project)?
62 .sources
63 .into_iter()
64 .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
66 .ok_or_else(|| eyre::eyre!("no Solidity sources"))?
67 .1
68 .into_iter()
69 .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
71 .unwrap();
72
73 self.preprocess_sources(&mut sources)?;
75
76 sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER));
78
79 let structs_to_write =
81 self.find_and_resolve_structs(&config, &project, version, sources, &target_path)?;
82
83 self.write_bindings(&structs_to_write, &target_path)?;
85
86 Ok(())
87 }
88
89 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 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 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 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 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 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 self.resolve_conflicts(&mut structs_to_write);
224
225 Ok(structs_to_write)
226 }
227
228 fn resolve_conflicts(&self, structs_to_write: &mut [StructToWrite]) {
235 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 let mut aliases = BTreeMap::new();
248
249 for (name, paths) in names_to_paths {
250 if paths.len() <= 1 {
251 continue; }
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 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 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 fn write_bindings(
297 &self,
298 structs_to_write: &[StructToWrite],
299 target_path: &PathBuf,
300 ) -> Result<()> {
301 let mut result = String::new();
302
303 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 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 result.push_str(
338 r#"
339library JsonBindings {
340 Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
341
342"#,
343 );
344
345 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 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 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 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 if var.mutability == Some(VarMut::Immutable) {
458 self.updates.push((var.span, ""));
459 }
460
461 self.walk_variable_definition(var)
462 }
463}
464
465#[derive(Debug, Clone)]
467struct StructToWrite {
468 name: String,
470 contract_name: Option<String>,
473 import_alias: Option<String>,
476 path: PathBuf,
478 schema: String,
480 name_in_fns: String,
482}
483
484impl StructToWrite {
485 fn struct_or_contract_name(&self) -> &str {
488 self.contract_name.as_deref().unwrap_or(&self.name)
489 }
490
491 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 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}