1use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param};
2use alloy_primitives::{hex, keccak256};
3use clap::Parser;
4use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
5use eyre::{Result, eyre};
6use foundry_cli::opts::{BuildOpts, CompilerOpts};
7use foundry_common::{
8 compile::{PathOrContractInfo, ProjectCompiler},
9 find_matching_contract_artifact, find_target_path, shell,
10};
11use foundry_compilers::{
12 ProjectCompileOutput,
13 artifacts::{
14 StorageLayout,
15 output_selection::{
16 BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
17 EvmOutputSelection, EwasmOutputSelection,
18 },
19 },
20 solc::SolcLanguage,
21};
22use path_slash::PathExt;
23use regex::Regex;
24use serde_json::{Map, Value};
25use solar::sema::interface::source_map::FileName;
26use std::{collections::BTreeMap, fmt, ops::ControlFlow, path::Path, str::FromStr, sync::LazyLock};
27
28#[derive(Clone, Debug, Parser)]
30pub struct InspectArgs {
31 #[arg(value_parser = PathOrContractInfo::from_str)]
33 pub contract: PathOrContractInfo,
34
35 #[arg(value_enum)]
37 pub field: ContractArtifactField,
38
39 #[command(flatten)]
41 build: BuildOpts,
42
43 #[arg(long, short, help_heading = "Display options")]
45 pub strip_yul_comments: bool,
46
47 #[arg(long, short, help_heading = "Display options")]
49 pub wrap: bool,
50}
51
52impl InspectArgs {
53 pub fn run(self) -> Result<()> {
54 let Self { contract, field, build, strip_yul_comments, wrap } = self;
55
56 trace!(target: "forge", ?field, ?contract, "running forge inspect");
57
58 let mut cos = build.compiler.extra_output;
60 if !field.can_skip_field() && !cos.iter().any(|selected| field == *selected) {
61 cos.push(field.try_into()?);
62 }
63
64 let optimized = if field == ContractArtifactField::AssemblyOptimized {
66 Some(true)
67 } else {
68 build.compiler.optimize
69 };
70
71 let solc_version = build.use_solc.clone();
73
74 let modified_build_args = BuildOpts {
76 compiler: CompilerOpts { extra_output: cos, optimize: optimized, ..build.compiler },
77 ..build
78 };
79
80 let project = modified_build_args.project()?;
82 let target_path = find_target_path(&project, &contract)?;
83 if field == ContractArtifactField::Linearization && !is_solidity_source(&target_path) {
84 eyre::bail!(
85 "linearization inspection is only supported for Solidity contracts (.sol targets)"
86 );
87 }
88 let compiler = ProjectCompiler::new().quiet(true);
89 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
90
91 let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
93
94 match field {
96 ContractArtifactField::Abi => {
97 let abi = artifact.abi.as_ref().ok_or_else(|| missing_error("ABI"))?;
98 print_abi(abi, wrap)?;
99 }
100 ContractArtifactField::Bytecode => {
101 print_json_str(&artifact.bytecode, Some("object"))?;
102 }
103 ContractArtifactField::DeployedBytecode => {
104 print_json_str(&artifact.deployed_bytecode, Some("object"))?;
105 }
106 ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => {
107 print_json_str(&artifact.assembly, None)?;
108 }
109 ContractArtifactField::LegacyAssembly => {
110 print_json_str(&artifact.legacy_assembly, None)?;
111 }
112 ContractArtifactField::MethodIdentifiers => {
113 print_method_identifiers(&artifact.method_identifiers, wrap)?;
114 }
115 ContractArtifactField::GasEstimates => {
116 print_json(&artifact.gas_estimates)?;
117 }
118 ContractArtifactField::StorageLayout => {
119 print_storage_layout(artifact.storage_layout.as_ref(), wrap)?;
120 }
121 ContractArtifactField::DevDoc => {
122 print_json(&artifact.devdoc)?;
123 }
124 ContractArtifactField::Ir => {
125 print_yul(artifact.ir.as_deref(), strip_yul_comments)?;
126 }
127 ContractArtifactField::IrOptimized => {
128 print_yul(artifact.ir_optimized.as_deref(), strip_yul_comments)?;
129 }
130 ContractArtifactField::Metadata => {
131 print_json(&artifact.metadata)?;
132 }
133 ContractArtifactField::UserDoc => {
134 print_json(&artifact.userdoc)?;
135 }
136 ContractArtifactField::Ewasm => {
137 print_json_str(&artifact.ewasm, None)?;
138 }
139 ContractArtifactField::Errors => {
140 let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors);
141 print_errors_events(&out, true, wrap)?;
142 }
143 ContractArtifactField::Events => {
144 let out = artifact.abi.as_ref().map_or(Map::new(), parse_events);
145 print_errors_events(&out, false, wrap)?;
146 }
147 ContractArtifactField::StandardJson => {
148 let standard_json = if let Some(version) = solc_version {
149 let version = version.parse()?;
150 let mut standard_json =
151 project.standard_json_input(&target_path)?.normalize_evm_version(&version);
152 standard_json.settings.sanitize(&version, SolcLanguage::Solidity);
153 standard_json
154 } else {
155 project.standard_json_input(&target_path)?
156 };
157 print_json(&standard_json)?;
158 }
159 ContractArtifactField::Libraries => {
160 let all_libs: Vec<String> = artifact
161 .all_link_references()
162 .into_iter()
163 .flat_map(|(path, libs)| {
164 libs.into_keys().map(move |lib| format!("{path}:{lib}"))
165 })
166 .collect();
167 if shell::is_json() {
168 return print_json(&all_libs);
169 }
170 sh_println!(
171 "Dynamically linked libraries:\n{}",
172 all_libs.iter().map(|v| format!(" {v}")).collect::<Vec<String>>().join("\n")
173 )?;
174 }
175 ContractArtifactField::Linearization => {
176 print_linearization(
177 &mut output,
178 project.root(),
179 &target_path,
180 contract.name(),
181 wrap,
182 )?;
183 }
184 };
185
186 Ok(())
187 }
188}
189
190fn parse_errors(abi: &JsonAbi) -> Map<String, Value> {
191 let mut out = serde_json::Map::new();
192 for er in abi.errors.values().flatten() {
193 let types = get_ty_sig(&er.inputs);
194 let sig = format!("{:x}", er.selector());
195 let sig_trimmed = &sig[0..8];
196 out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into());
197 }
198 out
199}
200
201fn parse_events(abi: &JsonAbi) -> Map<String, Value> {
202 let mut out = serde_json::Map::new();
203 for ev in abi.events.values().flatten() {
204 let types = parse_event_params(&ev.inputs);
205 let topic = hex::encode(keccak256(ev.signature()));
206 out.insert(format!("{}({})", ev.name, types), format!("0x{topic}").into());
207 }
208 out
209}
210
211fn parse_event_params(ev_params: &[EventParam]) -> String {
212 ev_params
213 .iter()
214 .map(|p| {
215 if let Some(ty) = p.internal_type() {
216 return internal_ty(ty);
217 }
218 p.ty.clone()
219 })
220 .collect::<Vec<_>>()
221 .join(",")
222}
223
224fn print_abi(abi: &JsonAbi, should_wrap: bool) -> Result<()> {
225 if shell::is_json() {
226 return print_json(abi);
227 }
228
229 let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")];
230 print_table(
231 headers,
232 |table| {
233 for ev in abi.events.values().flatten() {
235 let types = parse_event_params(&ev.inputs);
236 let selector = ev.selector().to_string();
237 table.add_row(["event", &format!("{}({})", ev.name, types), &selector]);
238 }
239
240 for er in abi.errors.values().flatten() {
242 let selector = er.selector().to_string();
243 table.add_row([
244 "error",
245 &format!("{}({})", er.name, get_ty_sig(&er.inputs)),
246 &selector,
247 ]);
248 }
249
250 for func in abi.functions.values().flatten() {
252 let selector = func.selector().to_string();
253 let state_mut = func.state_mutability.as_json_str();
254 let func_sig = if func.outputs.is_empty() {
255 format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs))
256 } else {
257 format!(
258 "{}({}) {state_mut} returns ({})",
259 func.name,
260 get_ty_sig(&func.inputs),
261 get_ty_sig(&func.outputs)
262 )
263 };
264 table.add_row(["function", &func_sig, &selector]);
265 }
266
267 if let Some(constructor) = abi.constructor() {
268 let state_mut = constructor.state_mutability.as_json_str();
269 table.add_row([
270 "constructor",
271 &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)),
272 "",
273 ]);
274 }
275
276 if let Some(fallback) = &abi.fallback {
277 let state_mut = fallback.state_mutability.as_json_str();
278 table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]);
279 }
280
281 if let Some(receive) = &abi.receive {
282 let state_mut = receive.state_mutability.as_json_str();
283 table.add_row(["receive", &format!("receive() {state_mut}"), ""]);
284 }
285 },
286 should_wrap,
287 )
288}
289
290fn get_ty_sig(inputs: &[Param]) -> String {
291 inputs
292 .iter()
293 .map(|p| {
294 if let Some(ty) = p.internal_type() {
295 return internal_ty(ty);
296 }
297 p.ty.clone()
298 })
299 .collect::<Vec<_>>()
300 .join(",")
301}
302
303fn internal_ty(ty: &InternalType) -> String {
304 let contract_ty =
305 |c: Option<&str>, ty: &String| c.map_or_else(|| ty.clone(), |c| format!("{c}.{ty}"));
306 match ty {
307 InternalType::AddressPayable(addr) => addr.clone(),
308 InternalType::Contract(contract) => contract.clone(),
309 InternalType::Enum { contract, ty } => contract_ty(contract.as_deref(), ty),
310 InternalType::Struct { contract, ty } => contract_ty(contract.as_deref(), ty),
311 InternalType::Other { contract, ty } => contract_ty(contract.as_deref(), ty),
312 }
313}
314
315pub fn print_storage_layout(
316 storage_layout: Option<&StorageLayout>,
317 should_wrap: bool,
318) -> Result<()> {
319 let Some(storage_layout) = storage_layout else {
320 return Err(missing_error("storage layout"));
321 };
322
323 if shell::is_json() {
324 return print_json(&storage_layout);
325 }
326
327 let headers = vec![
328 Cell::new("Name"),
329 Cell::new("Type"),
330 Cell::new("Slot"),
331 Cell::new("Offset"),
332 Cell::new("Bytes"),
333 Cell::new("Contract"),
334 ];
335
336 print_table(
337 headers,
338 |table| {
339 for slot in &storage_layout.storage {
340 let storage_type = storage_layout.types.get(&slot.storage_type);
341 table.add_row([
342 slot.label.as_str(),
343 storage_type.map_or("?", |t| &t.label),
344 &slot.slot,
345 &slot.offset.to_string(),
346 storage_type.map_or("?", |t| &t.number_of_bytes),
347 &slot.contract,
348 ]);
349 }
350 },
351 should_wrap,
352 )
353}
354
355fn print_method_identifiers(
356 method_identifiers: &Option<BTreeMap<String, String>>,
357 should_wrap: bool,
358) -> Result<()> {
359 let Some(method_identifiers) = method_identifiers else {
360 return Err(missing_error("method identifiers"));
361 };
362
363 if shell::is_json() {
364 return print_json(method_identifiers);
365 }
366
367 let headers = vec![Cell::new("Method"), Cell::new("Identifier")];
368
369 print_table(
370 headers,
371 |table| {
372 for (method, identifier) in method_identifiers {
373 table.add_row([method.as_str(), identifier.as_str()]);
374 }
375 },
376 should_wrap,
377 )
378}
379
380fn print_errors_events(map: &Map<String, Value>, is_err: bool, should_wrap: bool) -> Result<()> {
381 if shell::is_json() {
382 return print_json(map);
383 }
384
385 let headers = if is_err {
386 vec![Cell::new("Error"), Cell::new("Selector")]
387 } else {
388 vec![Cell::new("Event"), Cell::new("Topic")]
389 };
390 print_table(
391 headers,
392 |table| {
393 for (method, selector) in map {
394 table.add_row([method, selector.as_str().unwrap()]);
395 }
396 },
397 should_wrap,
398 )
399}
400
401fn print_table(
402 headers: Vec<Cell>,
403 add_rows: impl FnOnce(&mut Table),
404 should_wrap: bool,
405) -> Result<()> {
406 let mut table = Table::new();
407 if shell::is_markdown() {
408 table.load_preset(ASCII_MARKDOWN);
409 } else {
410 table.apply_modifier(UTF8_ROUND_CORNERS);
411 }
412 table.set_header(headers);
413 if should_wrap {
414 table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
415 }
416 add_rows(&mut table);
417 sh_println!("\n{table}\n")?;
418 Ok(())
419}
420
421fn print_linearization(
422 output: &mut ProjectCompileOutput,
423 root: &Path,
424 target_path: &Path,
425 target_name: Option<&str>,
426 should_wrap: bool,
427) -> Result<()> {
428 let mut chain = Vec::new();
429 let mut lowered = false;
430 let compiler = output.parser_mut().solc_mut().compiler_mut();
431 compiler.enter_mut(|compiler| -> Result<()> {
432 let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else { return Ok(()) };
433 lowered = true;
434
435 let hir = &compiler.gcx().hir;
436 let matching_contracts = hir
437 .contract_ids()
438 .filter(|id| {
439 let contract = hir.contract(*id);
440 if let Some(target_name) = target_name
441 && contract.name.as_str() != target_name
442 {
443 return false;
444 }
445
446 matches!(
447 &hir.source(contract.source).file.name,
448 FileName::Real(path) if path == target_path
449 )
450 })
451 .collect::<Vec<_>>();
452
453 let target_contract = match matching_contracts.as_slice() {
454 [id] => *id,
455 [] => {
456 if let Some(target_name) = target_name {
457 eyre::bail!(
458 "Could not find contract `{target_name}` in `{}`",
459 target_path.display()
460 );
461 }
462 eyre::bail!("Could not find contract in `{}`", target_path.display());
463 }
464 _ => {
465 eyre::bail!(
466 "Multiple contracts found in the same file, please specify the target <path>:<contract> or <contract>"
467 );
468 }
469 };
470
471 for (order, base_id) in hir.contract(target_contract).linearized_bases.iter().enumerate() {
472 let contract = hir.contract(*base_id);
473 let source = hir.source(contract.source);
474 let FileName::Real(path) = &source.file.name else { continue };
475 let path = path.strip_prefix(root).unwrap_or(path);
476 chain.push((
477 order,
478 path.to_slash_lossy().into_owned(),
479 contract.name.as_str().to_string(),
480 ));
481 }
482
483 Ok(())
484 })?;
485
486 let diags = compiler.sess().dcx.emitted_diagnostics().unwrap();
488 if compiler.sess().dcx.has_errors().is_err() {
489 eyre::bail!("{diags}");
490 } else {
491 let _ = sh_eprint!("{diags}");
492 }
493 if !lowered {
494 eyre::bail!(
495 "unable to inspect linearization: failed to lower Solidity ASTs for `{}`",
496 target_path.display()
497 );
498 }
499
500 if shell::is_json() {
501 let contracts = chain
502 .into_iter()
503 .map(|(order, source, contract)| {
504 serde_json::json!({
505 "order": order,
506 "source": source,
507 "contract": contract,
508 })
509 })
510 .collect::<Vec<_>>();
511 return print_json(&contracts);
512 }
513
514 let headers = vec![Cell::new("Order"), Cell::new("Source"), Cell::new("Contract")];
515 print_table(
516 headers,
517 |table| {
518 for (order, source, contract) in &chain {
519 table.add_row([order.to_string(), source.clone(), contract.clone()]);
520 }
521 },
522 should_wrap,
523 )
524}
525
526#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
528pub enum ContractArtifactField {
529 Abi,
530 Bytecode,
531 DeployedBytecode,
532 Assembly,
533 AssemblyOptimized,
534 LegacyAssembly,
535 MethodIdentifiers,
536 GasEstimates,
537 StorageLayout,
538 DevDoc,
539 Ir,
540 IrOptimized,
541 Metadata,
542 UserDoc,
543 Ewasm,
544 Errors,
545 Events,
546 StandardJson,
547 Libraries,
548 Linearization,
549}
550
551macro_rules! impl_value_enum {
552 (enum $name:ident { $($field:ident => $main:literal $(| $alias:literal)*),+ $(,)? }) => {
553 impl $name {
554 pub const ALL: &'static [Self] = &[$(Self::$field),+];
556
557 pub const fn as_str(&self) -> &'static str {
559 match self {
560 $(
561 Self::$field => $main,
562 )+
563 }
564 }
565
566 pub const fn aliases(&self) -> &'static [&'static str] {
568 match self {
569 $(
570 Self::$field => &[$($alias),*],
571 )+
572 }
573 }
574 }
575
576 impl ::clap::ValueEnum for $name {
577 fn value_variants<'a>() -> &'a [Self] {
578 Self::ALL
579 }
580
581 fn to_possible_value(&self) -> Option<::clap::builder::PossibleValue> {
582 Some(::clap::builder::PossibleValue::new(Self::as_str(self)).aliases(Self::aliases(self)))
583 }
584
585 fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
586 let _ = ignore_case;
587 <Self as ::std::str::FromStr>::from_str(input)
588 }
589 }
590
591 impl ::std::str::FromStr for $name {
592 type Err = String;
593
594 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 match s {
596 $(
597 $main $(| $alias)* => Ok(Self::$field),
598 )+
599 _ => Err(format!(concat!("Invalid ", stringify!($name), " value: {}"), s)),
600 }
601 }
602 }
603 };
604}
605
606impl_value_enum! {
607 enum ContractArtifactField {
608 Abi => "abi",
609 Bytecode => "bytecode" | "bytes" | "b",
610 DeployedBytecode => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode"
611 | "deployed" | "deployedbytecode",
612 Assembly => "assembly" | "asm",
613 LegacyAssembly => "legacyAssembly" | "legacyassembly" | "legacy_assembly",
614 AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized"
615 | "assembly_optimized" | "asmopt" | "assembly-optimized"
616 | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized",
617 MethodIdentifiers => "methodIdentifiers" | "methodidentifiers" | "methods"
618 | "method_identifiers" | "method-identifiers" | "mi",
619 GasEstimates => "gasEstimates" | "gas" | "gas_estimates" | "gas-estimates"
620 | "gasestimates",
621 StorageLayout => "storageLayout" | "storage_layout" | "storage-layout"
622 | "storagelayout" | "storage",
623 DevDoc => "devdoc" | "dev-doc" | "devDoc",
624 Ir => "ir" | "iR" | "IR",
625 IrOptimized => "irOptimized" | "ir-optimized" | "iroptimized" | "iro" | "iropt",
626 Metadata => "metadata" | "meta",
627 UserDoc => "userdoc" | "userDoc" | "user-doc",
628 Ewasm => "ewasm" | "e-wasm",
629 Errors => "errors" | "er",
630 Events => "events" | "ev",
631 StandardJson => "standardJson" | "standard-json" | "standard_json",
632 Libraries => "libraries" | "lib" | "libs",
633 Linearization => "linearization" | "linearizedInheritance"
634 | "linearized-inheritance" | "linearized_inheritance"
635 | "linearizedBases" | "linearized-bases" | "linearized_bases",
636 }
637}
638
639impl TryFrom<ContractArtifactField> for ContractOutputSelection {
640 type Error = eyre::Error;
641
642 fn try_from(field: ContractArtifactField) -> Result<Self, Self::Error> {
643 type Caf = ContractArtifactField;
644 match field {
645 Caf::Abi => Ok(Self::Abi),
646 Caf::Bytecode => {
647 Ok(Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)))
648 }
649 Caf::DeployedBytecode => Ok(Self::Evm(EvmOutputSelection::DeployedByteCode(
650 DeployedBytecodeOutputSelection::All,
651 ))),
652 Caf::Assembly | Caf::AssemblyOptimized => Ok(Self::Evm(EvmOutputSelection::Assembly)),
653 Caf::LegacyAssembly => Ok(Self::Evm(EvmOutputSelection::LegacyAssembly)),
654 Caf::MethodIdentifiers => Ok(Self::Evm(EvmOutputSelection::MethodIdentifiers)),
655 Caf::GasEstimates => Ok(Self::Evm(EvmOutputSelection::GasEstimates)),
656 Caf::StorageLayout => Ok(Self::StorageLayout),
657 Caf::DevDoc => Ok(Self::DevDoc),
658 Caf::Ir => Ok(Self::Ir),
659 Caf::IrOptimized => Ok(Self::IrOptimized),
660 Caf::Metadata => Ok(Self::Metadata),
661 Caf::UserDoc => Ok(Self::UserDoc),
662 Caf::Ewasm => Ok(Self::Ewasm(EwasmOutputSelection::All)),
663 Caf::Errors => Ok(Self::Abi),
664 Caf::Events => Ok(Self::Abi),
665 Caf::StandardJson => {
666 Err(eyre!("StandardJson is not supported for ContractOutputSelection"))
667 }
668 Caf::Libraries => Err(eyre!("Libraries is not supported for ContractOutputSelection")),
669 Caf::Linearization => {
670 Err(eyre!("Linearization is not supported for ContractOutputSelection"))
671 }
672 }
673 }
674}
675
676impl PartialEq<ContractOutputSelection> for ContractArtifactField {
677 fn eq(&self, other: &ContractOutputSelection) -> bool {
678 type Cos = ContractOutputSelection;
679 type Eos = EvmOutputSelection;
680 matches!(
681 (self, other),
682 (Self::Abi | Self::Events | Self::Errors, Cos::Abi)
683 | (Self::Bytecode, Cos::Evm(Eos::ByteCode(_)))
684 | (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_)))
685 | (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly))
686 | (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly))
687 | (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers))
688 | (Self::GasEstimates, Cos::Evm(Eos::GasEstimates))
689 | (Self::StorageLayout, Cos::StorageLayout)
690 | (Self::DevDoc, Cos::DevDoc)
691 | (Self::Ir, Cos::Ir)
692 | (Self::IrOptimized, Cos::IrOptimized)
693 | (Self::Metadata, Cos::Metadata)
694 | (Self::UserDoc, Cos::UserDoc)
695 | (Self::Ewasm, Cos::Ewasm(_))
696 )
697 }
698}
699
700impl fmt::Display for ContractArtifactField {
701 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702 f.write_str(self.as_str())
703 }
704}
705
706impl ContractArtifactField {
707 pub const fn can_skip_field(&self) -> bool {
709 matches!(
710 self,
711 Self::Bytecode
712 | Self::DeployedBytecode
713 | Self::StandardJson
714 | Self::Libraries
715 | Self::Linearization
716 )
717 }
718}
719
720fn print_json(obj: &impl serde::Serialize) -> Result<()> {
721 sh_println!("{}", serde_json::to_string_pretty(obj)?)?;
722 Ok(())
723}
724
725fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> {
726 sh_println!("{}", get_json_str(obj, key)?)?;
727 Ok(())
728}
729
730fn print_yul(yul: Option<&str>, strip_comments: bool) -> Result<()> {
731 let Some(yul) = yul else {
732 return Err(missing_error("IR output"));
733 };
734
735 static YUL_COMMENTS: LazyLock<Regex> =
736 LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*?\*/)").unwrap());
737
738 if strip_comments {
739 sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?;
740 } else {
741 sh_println!("{yul}")?;
742 }
743
744 Ok(())
745}
746
747fn get_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<String> {
748 let value = serde_json::to_value(obj)?;
749 let value = if let Some(key) = key
750 && let Some(value) = value.get(key)
751 {
752 value
753 } else {
754 &value
755 };
756 Ok(match value.as_str() {
757 Some(s) => s.to_string(),
758 None => format!("{value:#}"),
759 })
760}
761
762fn is_solidity_source(path: &Path) -> bool {
763 path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| ext.eq_ignore_ascii_case("sol"))
764}
765
766fn missing_error(field: &str) -> eyre::Error {
767 eyre!(
768 "{field} missing from artifact; \
769 this could be a spurious caching issue, consider running `forge clean`"
770 )
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn contract_output_selection() {
779 for &field in ContractArtifactField::ALL {
780 if field == ContractArtifactField::StandardJson {
781 let selection: Result<ContractOutputSelection, _> = field.try_into();
782 assert!(
783 selection
784 .unwrap_err()
785 .to_string()
786 .eq("StandardJson is not supported for ContractOutputSelection")
787 );
788 } else if field == ContractArtifactField::Libraries {
789 let selection: Result<ContractOutputSelection, _> = field.try_into();
790 assert!(
791 selection
792 .unwrap_err()
793 .to_string()
794 .eq("Libraries is not supported for ContractOutputSelection")
795 );
796 } else if field == ContractArtifactField::Linearization {
797 let selection: Result<ContractOutputSelection, _> = field.try_into();
798 assert!(
799 selection
800 .unwrap_err()
801 .to_string()
802 .eq("Linearization is not supported for ContractOutputSelection")
803 );
804 } else {
805 let selection: ContractOutputSelection = field.try_into().unwrap();
806 assert_eq!(field, selection);
807
808 let s = field.as_str();
809 assert_eq!(s, field.to_string());
810 assert_eq!(s.parse::<ContractArtifactField>().unwrap(), field);
811 for alias in field.aliases() {
812 assert_eq!(alias.parse::<ContractArtifactField>().unwrap(), field);
813 }
814 }
815 }
816 }
817}