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