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