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