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