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