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#[derive(Clone, Debug, Parser)]
40pub struct BindJsonArgs {
41 #[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 let sources = project.paths.read_input_files()?;
57 let graph = Graph::<MultiCompilerParsedSource>::resolve_sources(&project.paths, sources)?;
58
59 let (version, mut sources, _) = graph
61 .into_sources_by_version(&project)?
63 .sources
64 .into_iter()
65 .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
67 .ok_or_else(|| eyre::eyre!("no Solidity sources"))?
68 .1
69 .into_iter()
70 .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
72 .unwrap();
73
74 self.preprocess_sources(&mut sources)?;
76
77 sources.insert(target_path.clone(), Source::new(JSON_BINDINGS_PLACEHOLDER));
79
80 let structs_to_write =
82 self.find_and_resolve_structs(&config, &project, version, sources, &target_path)?;
83
84 self.write_bindings(&structs_to_write, &target_path)?;
86
87 Ok(())
88 }
89
90 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 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 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 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 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 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 self.resolve_conflicts(&mut structs_to_write);
222
223 Ok(structs_to_write)
224 }
225
226 fn resolve_conflicts(&self, structs_to_write: &mut [StructToWrite]) {
233 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 let mut aliases = BTreeMap::new();
246
247 for (name, paths) in names_to_paths {
248 if paths.len() <= 1 {
249 continue; }
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 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 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 fn write_bindings(
295 &self,
296 structs_to_write: &[StructToWrite],
297 target_path: &PathBuf,
298 ) -> Result<()> {
299 let mut result = String::new();
300
301 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 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 result.push_str(
336 r#"
337library JsonBindings {
338 Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
339
340"#,
341 );
342
343 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 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 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 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 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#[derive(Debug, Clone)]
465struct StructToWrite {
466 name: String,
468 contract_name: Option<String>,
471 import_alias: Option<String>,
474 path: PathBuf,
476 schema: String,
478 name_in_fns: String,
480}
481
482impl StructToWrite {
483 fn struct_or_contract_name(&self) -> &str {
486 self.contract_name.as_deref().unwrap_or(&self.name)
487 }
488
489 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 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}