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