1use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param};
2use alloy_primitives::{hex, keccak256};
3use clap::Parser;
4use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
5use eyre::{Result, eyre};
6use foundry_cli::opts::{BuildOpts, CompilerOpts};
7use foundry_common::{
8 compile::{PathOrContractInfo, ProjectCompiler},
9 find_matching_contract_artifact, find_target_path, shell,
10};
11use foundry_compilers::{
12 artifacts::{
13 StorageLayout,
14 output_selection::{
15 BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
16 EvmOutputSelection, EwasmOutputSelection,
17 },
18 },
19 solc::SolcLanguage,
20};
21use regex::Regex;
22use serde_json::{Map, Value};
23use std::{collections::BTreeMap, fmt, str::FromStr, sync::LazyLock};
24
25#[derive(Clone, Debug, Parser)]
27pub struct InspectArgs {
28 #[arg(value_parser = PathOrContractInfo::from_str)]
30 pub contract: PathOrContractInfo,
31
32 #[arg(value_enum)]
34 pub field: ContractArtifactField,
35
36 #[command(flatten)]
38 build: BuildOpts,
39
40 #[arg(long, short, help_heading = "Display options")]
42 pub strip_yul_comments: bool,
43
44 #[arg(long, short, help_heading = "Display options")]
46 pub wrap: bool,
47}
48
49impl InspectArgs {
50 pub fn run(self) -> Result<()> {
51 let Self { contract, field, build, strip_yul_comments, wrap } = self;
52
53 trace!(target: "forge", ?field, ?contract, "running forge inspect");
54
55 let mut cos = build.compiler.extra_output;
57 if !field.can_skip_field() && !cos.iter().any(|selected| field == *selected) {
58 cos.push(field.try_into()?);
59 }
60
61 let optimized = if field == ContractArtifactField::AssemblyOptimized {
63 Some(true)
64 } else {
65 build.compiler.optimize
66 };
67
68 let solc_version = build.use_solc.clone();
70
71 let modified_build_args = BuildOpts {
73 compiler: CompilerOpts { extra_output: cos, optimize: optimized, ..build.compiler },
74 ..build
75 };
76
77 let project = modified_build_args.project()?;
79 let compiler = ProjectCompiler::new().quiet(true);
80 let target_path = find_target_path(&project, &contract)?;
81 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
82
83 let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
85
86 match field {
88 ContractArtifactField::Abi => {
89 let abi = artifact.abi.as_ref().ok_or_else(|| missing_error("ABI"))?;
90 print_abi(abi, wrap)?;
91 }
92 ContractArtifactField::Bytecode => {
93 print_json_str(&artifact.bytecode, Some("object"))?;
94 }
95 ContractArtifactField::DeployedBytecode => {
96 print_json_str(&artifact.deployed_bytecode, Some("object"))?;
97 }
98 ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => {
99 print_json_str(&artifact.assembly, None)?;
100 }
101 ContractArtifactField::LegacyAssembly => {
102 print_json_str(&artifact.legacy_assembly, None)?;
103 }
104 ContractArtifactField::MethodIdentifiers => {
105 print_method_identifiers(&artifact.method_identifiers, wrap)?;
106 }
107 ContractArtifactField::GasEstimates => {
108 print_json(&artifact.gas_estimates)?;
109 }
110 ContractArtifactField::StorageLayout => {
111 print_storage_layout(artifact.storage_layout.as_ref(), wrap)?;
112 }
113 ContractArtifactField::DevDoc => {
114 print_json(&artifact.devdoc)?;
115 }
116 ContractArtifactField::Ir => {
117 print_yul(artifact.ir.as_deref(), strip_yul_comments)?;
118 }
119 ContractArtifactField::IrOptimized => {
120 print_yul(artifact.ir_optimized.as_deref(), strip_yul_comments)?;
121 }
122 ContractArtifactField::Metadata => {
123 print_json(&artifact.metadata)?;
124 }
125 ContractArtifactField::UserDoc => {
126 print_json(&artifact.userdoc)?;
127 }
128 ContractArtifactField::Ewasm => {
129 print_json_str(&artifact.ewasm, None)?;
130 }
131 ContractArtifactField::Errors => {
132 let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors);
133 print_errors_events(&out, true, wrap)?;
134 }
135 ContractArtifactField::Events => {
136 let out = artifact.abi.as_ref().map_or(Map::new(), parse_events);
137 print_errors_events(&out, false, wrap)?;
138 }
139 ContractArtifactField::StandardJson => {
140 let standard_json = if let Some(version) = solc_version {
141 let version = version.parse()?;
142 let mut standard_json =
143 project.standard_json_input(&target_path)?.normalize_evm_version(&version);
144 standard_json.settings.sanitize(&version, SolcLanguage::Solidity);
145 standard_json
146 } else {
147 project.standard_json_input(&target_path)?
148 };
149 print_json(&standard_json)?;
150 }
151 ContractArtifactField::Libraries => {
152 let all_libs: Vec<String> = artifact
153 .all_link_references()
154 .into_iter()
155 .flat_map(|(path, libs)| {
156 libs.into_keys().map(move |lib| format!("{path}:{lib}"))
157 })
158 .collect();
159 if shell::is_json() {
160 return print_json(&all_libs);
161 } else {
162 sh_println!(
163 "Dynamically linked libraries:\n{}",
164 all_libs
165 .iter()
166 .map(|v| format!(" {v}"))
167 .collect::<Vec<String>>()
168 .join("\n")
169 )?;
170 }
171 }
172 };
173
174 Ok(())
175 }
176}
177
178fn parse_errors(abi: &JsonAbi) -> Map<String, Value> {
179 let mut out = serde_json::Map::new();
180 for er in abi.errors.iter().flat_map(|(_, errors)| errors) {
181 let types = get_ty_sig(&er.inputs);
182 let sig = format!("{:x}", er.selector());
183 let sig_trimmed = &sig[0..8];
184 out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into());
185 }
186 out
187}
188
189fn parse_events(abi: &JsonAbi) -> Map<String, Value> {
190 let mut out = serde_json::Map::new();
191 for ev in abi.events.iter().flat_map(|(_, events)| events) {
192 let types = parse_event_params(&ev.inputs);
193 let topic = hex::encode(keccak256(ev.signature()));
194 out.insert(format!("{}({})", ev.name, types), format!("0x{topic}").into());
195 }
196 out
197}
198
199fn parse_event_params(ev_params: &[EventParam]) -> String {
200 ev_params
201 .iter()
202 .map(|p| {
203 if let Some(ty) = p.internal_type() {
204 return internal_ty(ty);
205 }
206 p.ty.clone()
207 })
208 .collect::<Vec<_>>()
209 .join(",")
210}
211
212fn print_abi(abi: &JsonAbi, should_wrap: bool) -> Result<()> {
213 if shell::is_json() {
214 return print_json(abi);
215 }
216
217 let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")];
218 print_table(
219 headers,
220 |table| {
221 for ev in abi.events.iter().flat_map(|(_, events)| events) {
223 let types = parse_event_params(&ev.inputs);
224 let selector = ev.selector().to_string();
225 table.add_row(["event", &format!("{}({})", ev.name, types), &selector]);
226 }
227
228 for er in abi.errors.iter().flat_map(|(_, errors)| errors) {
230 let selector = er.selector().to_string();
231 table.add_row([
232 "error",
233 &format!("{}({})", er.name, get_ty_sig(&er.inputs)),
234 &selector,
235 ]);
236 }
237
238 for func in abi.functions.iter().flat_map(|(_, f)| f) {
240 let selector = func.selector().to_string();
241 let state_mut = func.state_mutability.as_json_str();
242 let func_sig = if !func.outputs.is_empty() {
243 format!(
244 "{}({}) {state_mut} returns ({})",
245 func.name,
246 get_ty_sig(&func.inputs),
247 get_ty_sig(&func.outputs)
248 )
249 } else {
250 format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs))
251 };
252 table.add_row(["function", &func_sig, &selector]);
253 }
254
255 if let Some(constructor) = abi.constructor() {
256 let state_mut = constructor.state_mutability.as_json_str();
257 table.add_row([
258 "constructor",
259 &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)),
260 "",
261 ]);
262 }
263
264 if let Some(fallback) = &abi.fallback {
265 let state_mut = fallback.state_mutability.as_json_str();
266 table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]);
267 }
268
269 if let Some(receive) = &abi.receive {
270 let state_mut = receive.state_mutability.as_json_str();
271 table.add_row(["receive", &format!("receive() {state_mut}"), ""]);
272 }
273 },
274 should_wrap,
275 )
276}
277
278fn get_ty_sig(inputs: &[Param]) -> String {
279 inputs
280 .iter()
281 .map(|p| {
282 if let Some(ty) = p.internal_type() {
283 return internal_ty(ty);
284 }
285 p.ty.clone()
286 })
287 .collect::<Vec<_>>()
288 .join(",")
289}
290
291fn internal_ty(ty: &InternalType) -> String {
292 let contract_ty =
293 |c: Option<&str>, ty: &String| c.map_or_else(|| ty.clone(), |c| format!("{c}.{ty}"));
294 match ty {
295 InternalType::AddressPayable(addr) => addr.clone(),
296 InternalType::Contract(contract) => contract.clone(),
297 InternalType::Enum { contract, ty } => contract_ty(contract.as_deref(), ty),
298 InternalType::Struct { contract, ty } => contract_ty(contract.as_deref(), ty),
299 InternalType::Other { contract, ty } => contract_ty(contract.as_deref(), ty),
300 }
301}
302
303pub fn print_storage_layout(
304 storage_layout: Option<&StorageLayout>,
305 should_wrap: bool,
306) -> Result<()> {
307 let Some(storage_layout) = storage_layout else {
308 return Err(missing_error("storage layout"));
309 };
310
311 if shell::is_json() {
312 return print_json(&storage_layout);
313 }
314
315 let headers = vec![
316 Cell::new("Name"),
317 Cell::new("Type"),
318 Cell::new("Slot"),
319 Cell::new("Offset"),
320 Cell::new("Bytes"),
321 Cell::new("Contract"),
322 ];
323
324 print_table(
325 headers,
326 |table| {
327 for slot in &storage_layout.storage {
328 let storage_type = storage_layout.types.get(&slot.storage_type);
329 table.add_row([
330 slot.label.as_str(),
331 storage_type.map_or("?", |t| &t.label),
332 &slot.slot,
333 &slot.offset.to_string(),
334 storage_type.map_or("?", |t| &t.number_of_bytes),
335 &slot.contract,
336 ]);
337 }
338 },
339 should_wrap,
340 )
341}
342
343fn print_method_identifiers(
344 method_identifiers: &Option<BTreeMap<String, String>>,
345 should_wrap: bool,
346) -> Result<()> {
347 let Some(method_identifiers) = method_identifiers else {
348 return Err(missing_error("method identifiers"));
349 };
350
351 if shell::is_json() {
352 return print_json(method_identifiers);
353 }
354
355 let headers = vec![Cell::new("Method"), Cell::new("Identifier")];
356
357 print_table(
358 headers,
359 |table| {
360 for (method, identifier) in method_identifiers {
361 table.add_row([method, identifier]);
362 }
363 },
364 should_wrap,
365 )
366}
367
368fn print_errors_events(map: &Map<String, Value>, is_err: bool, should_wrap: bool) -> Result<()> {
369 if shell::is_json() {
370 return print_json(map);
371 }
372
373 let headers = if is_err {
374 vec![Cell::new("Error"), Cell::new("Selector")]
375 } else {
376 vec![Cell::new("Event"), Cell::new("Topic")]
377 };
378 print_table(
379 headers,
380 |table| {
381 for (method, selector) in map {
382 table.add_row([method, selector.as_str().unwrap()]);
383 }
384 },
385 should_wrap,
386 )
387}
388
389fn print_table(
390 headers: Vec<Cell>,
391 add_rows: impl FnOnce(&mut Table),
392 should_wrap: bool,
393) -> Result<()> {
394 let mut table = Table::new();
395 if shell::is_markdown() {
396 table.load_preset(ASCII_MARKDOWN);
397 } else {
398 table.apply_modifier(UTF8_ROUND_CORNERS);
399 }
400 table.set_header(headers);
401 if should_wrap {
402 table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
403 }
404 add_rows(&mut table);
405 sh_println!("\n{table}\n")?;
406 Ok(())
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
411pub enum ContractArtifactField {
412 Abi,
413 Bytecode,
414 DeployedBytecode,
415 Assembly,
416 AssemblyOptimized,
417 LegacyAssembly,
418 MethodIdentifiers,
419 GasEstimates,
420 StorageLayout,
421 DevDoc,
422 Ir,
423 IrOptimized,
424 Metadata,
425 UserDoc,
426 Ewasm,
427 Errors,
428 Events,
429 StandardJson,
430 Libraries,
431}
432
433macro_rules! impl_value_enum {
434 (enum $name:ident { $($field:ident => $main:literal $(| $alias:literal)*),+ $(,)? }) => {
435 impl $name {
436 pub const ALL: &'static [Self] = &[$(Self::$field),+];
438
439 pub const fn as_str(&self) -> &'static str {
441 match self {
442 $(
443 Self::$field => $main,
444 )+
445 }
446 }
447
448 pub const fn aliases(&self) -> &'static [&'static str] {
450 match self {
451 $(
452 Self::$field => &[$($alias),*],
453 )+
454 }
455 }
456 }
457
458 impl ::clap::ValueEnum for $name {
459 fn value_variants<'a>() -> &'a [Self] {
460 Self::ALL
461 }
462
463 fn to_possible_value(&self) -> Option<::clap::builder::PossibleValue> {
464 Some(::clap::builder::PossibleValue::new(Self::as_str(self)).aliases(Self::aliases(self)))
465 }
466
467 fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
468 let _ = ignore_case;
469 <Self as ::std::str::FromStr>::from_str(input)
470 }
471 }
472
473 impl ::std::str::FromStr for $name {
474 type Err = String;
475
476 fn from_str(s: &str) -> Result<Self, Self::Err> {
477 match s {
478 $(
479 $main $(| $alias)* => Ok(Self::$field),
480 )+
481 _ => Err(format!(concat!("Invalid ", stringify!($name), " value: {}"), s)),
482 }
483 }
484 }
485 };
486}
487
488impl_value_enum! {
489 enum ContractArtifactField {
490 Abi => "abi",
491 Bytecode => "bytecode" | "bytes" | "b",
492 DeployedBytecode => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode"
493 | "deployed" | "deployedbytecode",
494 Assembly => "assembly" | "asm",
495 LegacyAssembly => "legacyAssembly" | "legacyassembly" | "legacy_assembly",
496 AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized"
497 | "assembly_optimized" | "asmopt" | "assembly-optimized"
498 | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized",
499 MethodIdentifiers => "methodIdentifiers" | "methodidentifiers" | "methods"
500 | "method_identifiers" | "method-identifiers" | "mi",
501 GasEstimates => "gasEstimates" | "gas" | "gas_estimates" | "gas-estimates"
502 | "gasestimates",
503 StorageLayout => "storageLayout" | "storage_layout" | "storage-layout"
504 | "storagelayout" | "storage",
505 DevDoc => "devdoc" | "dev-doc" | "devDoc",
506 Ir => "ir" | "iR" | "IR",
507 IrOptimized => "irOptimized" | "ir-optimized" | "iroptimized" | "iro" | "iropt",
508 Metadata => "metadata" | "meta",
509 UserDoc => "userdoc" | "userDoc" | "user-doc",
510 Ewasm => "ewasm" | "e-wasm",
511 Errors => "errors" | "er",
512 Events => "events" | "ev",
513 StandardJson => "standardJson" | "standard-json" | "standard_json",
514 Libraries => "libraries" | "lib" | "libs",
515 }
516}
517
518impl TryFrom<ContractArtifactField> for ContractOutputSelection {
519 type Error = eyre::Error;
520
521 fn try_from(field: ContractArtifactField) -> Result<Self, Self::Error> {
522 type Caf = ContractArtifactField;
523 match field {
524 Caf::Abi => Ok(Self::Abi),
525 Caf::Bytecode => {
526 Ok(Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)))
527 }
528 Caf::DeployedBytecode => Ok(Self::Evm(EvmOutputSelection::DeployedByteCode(
529 DeployedBytecodeOutputSelection::All,
530 ))),
531 Caf::Assembly | Caf::AssemblyOptimized => Ok(Self::Evm(EvmOutputSelection::Assembly)),
532 Caf::LegacyAssembly => Ok(Self::Evm(EvmOutputSelection::LegacyAssembly)),
533 Caf::MethodIdentifiers => Ok(Self::Evm(EvmOutputSelection::MethodIdentifiers)),
534 Caf::GasEstimates => Ok(Self::Evm(EvmOutputSelection::GasEstimates)),
535 Caf::StorageLayout => Ok(Self::StorageLayout),
536 Caf::DevDoc => Ok(Self::DevDoc),
537 Caf::Ir => Ok(Self::Ir),
538 Caf::IrOptimized => Ok(Self::IrOptimized),
539 Caf::Metadata => Ok(Self::Metadata),
540 Caf::UserDoc => Ok(Self::UserDoc),
541 Caf::Ewasm => Ok(Self::Ewasm(EwasmOutputSelection::All)),
542 Caf::Errors => Ok(Self::Abi),
543 Caf::Events => Ok(Self::Abi),
544 Caf::StandardJson => {
545 Err(eyre!("StandardJson is not supported for ContractOutputSelection"))
546 }
547 Caf::Libraries => Err(eyre!("Libraries is not supported for ContractOutputSelection")),
548 }
549 }
550}
551
552impl PartialEq<ContractOutputSelection> for ContractArtifactField {
553 fn eq(&self, other: &ContractOutputSelection) -> bool {
554 type Cos = ContractOutputSelection;
555 type Eos = EvmOutputSelection;
556 matches!(
557 (self, other),
558 (Self::Abi | Self::Events, Cos::Abi)
559 | (Self::Errors, Cos::Abi)
560 | (Self::Bytecode, Cos::Evm(Eos::ByteCode(_)))
561 | (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_)))
562 | (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly))
563 | (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly))
564 | (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers))
565 | (Self::GasEstimates, Cos::Evm(Eos::GasEstimates))
566 | (Self::StorageLayout, Cos::StorageLayout)
567 | (Self::DevDoc, Cos::DevDoc)
568 | (Self::Ir, Cos::Ir)
569 | (Self::IrOptimized, Cos::IrOptimized)
570 | (Self::Metadata, Cos::Metadata)
571 | (Self::UserDoc, Cos::UserDoc)
572 | (Self::Ewasm, Cos::Ewasm(_))
573 )
574 }
575}
576
577impl fmt::Display for ContractArtifactField {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 f.write_str(self.as_str())
580 }
581}
582
583impl ContractArtifactField {
584 pub const fn can_skip_field(&self) -> bool {
586 matches!(
587 self,
588 Self::Bytecode | Self::DeployedBytecode | Self::StandardJson | Self::Libraries
589 )
590 }
591}
592
593fn print_json(obj: &impl serde::Serialize) -> Result<()> {
594 sh_println!("{}", serde_json::to_string_pretty(obj)?)?;
595 Ok(())
596}
597
598fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> {
599 sh_println!("{}", get_json_str(obj, key)?)?;
600 Ok(())
601}
602
603fn print_yul(yul: Option<&str>, strip_comments: bool) -> Result<()> {
604 let Some(yul) = yul else {
605 return Err(missing_error("IR output"));
606 };
607
608 static YUL_COMMENTS: LazyLock<Regex> =
609 LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*?\*/)").unwrap());
610
611 if strip_comments {
612 sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?;
613 } else {
614 sh_println!("{yul}")?;
615 }
616
617 Ok(())
618}
619
620fn get_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<String> {
621 let value = serde_json::to_value(obj)?;
622 let value = if let Some(key) = key
623 && let Some(value) = value.get(key)
624 {
625 value
626 } else {
627 &value
628 };
629 Ok(match value.as_str() {
630 Some(s) => s.to_string(),
631 None => format!("{value:#}"),
632 })
633}
634
635fn missing_error(field: &str) -> eyre::Error {
636 eyre!(
637 "{field} missing from artifact; \
638 this could be a spurious caching issue, consider running `forge clean`"
639 )
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn contract_output_selection() {
648 for &field in ContractArtifactField::ALL {
649 if field == ContractArtifactField::StandardJson {
650 let selection: Result<ContractOutputSelection, _> = field.try_into();
651 assert!(
652 selection
653 .unwrap_err()
654 .to_string()
655 .eq("StandardJson is not supported for ContractOutputSelection")
656 );
657 } else if field == ContractArtifactField::Libraries {
658 let selection: Result<ContractOutputSelection, _> = field.try_into();
659 assert!(
660 selection
661 .unwrap_err()
662 .to_string()
663 .eq("Libraries is not supported for ContractOutputSelection")
664 );
665 } else {
666 let selection: ContractOutputSelection = field.try_into().unwrap();
667 assert_eq!(field, selection);
668
669 let s = field.as_str();
670 assert_eq!(s, field.to_string());
671 assert_eq!(s.parse::<ContractArtifactField>().unwrap(), field);
672 for alias in field.aliases() {
673 assert_eq!(alias.parse::<ContractArtifactField>().unwrap(), field);
674 }
675 }
676 }
677 }
678}