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