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