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 };
152
153 Ok(())
154 }
155}
156
157fn parse_errors(abi: &JsonAbi) -> Map<String, Value> {
158 let mut out = serde_json::Map::new();
159 for er in abi.errors.iter().flat_map(|(_, errors)| errors) {
160 let types = get_ty_sig(&er.inputs);
161 let sig = format!("{:x}", er.selector());
162 let sig_trimmed = &sig[0..8];
163 out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into());
164 }
165 out
166}
167
168fn parse_events(abi: &JsonAbi) -> Map<String, Value> {
169 let mut out = serde_json::Map::new();
170 for ev in abi.events.iter().flat_map(|(_, events)| events) {
171 let types = parse_event_params(&ev.inputs);
172 let topic = hex::encode(keccak256(ev.signature()));
173 out.insert(format!("{}({})", ev.name, types), format!("0x{topic}").into());
174 }
175 out
176}
177
178fn parse_event_params(ev_params: &[EventParam]) -> String {
179 ev_params
180 .iter()
181 .map(|p| {
182 if let Some(ty) = p.internal_type() {
183 return internal_ty(ty);
184 }
185 p.ty.clone()
186 })
187 .collect::<Vec<_>>()
188 .join(",")
189}
190
191fn print_abi(abi: &JsonAbi, should_wrap: bool) -> Result<()> {
192 if shell::is_json() {
193 return print_json(abi);
194 }
195
196 let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")];
197 print_table(
198 headers,
199 |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 should_wrap,
254 )
255}
256
257fn get_ty_sig(inputs: &[Param]) -> String {
258 inputs
259 .iter()
260 .map(|p| {
261 if let Some(ty) = p.internal_type() {
262 return internal_ty(ty);
263 }
264 p.ty.clone()
265 })
266 .collect::<Vec<_>>()
267 .join(",")
268}
269
270fn internal_ty(ty: &InternalType) -> String {
271 let contract_ty =
272 |c: Option<&str>, ty: &String| c.map_or_else(|| ty.clone(), |c| format!("{c}.{ty}"));
273 match ty {
274 InternalType::AddressPayable(addr) => addr.clone(),
275 InternalType::Contract(contract) => contract.clone(),
276 InternalType::Enum { contract, ty } => contract_ty(contract.as_deref(), ty),
277 InternalType::Struct { contract, ty } => contract_ty(contract.as_deref(), ty),
278 InternalType::Other { contract, ty } => contract_ty(contract.as_deref(), ty),
279 }
280}
281
282pub fn print_storage_layout(
283 storage_layout: Option<&StorageLayout>,
284 should_wrap: bool,
285) -> Result<()> {
286 let Some(storage_layout) = storage_layout else {
287 return Err(missing_error("storage layout"));
288 };
289
290 if shell::is_json() {
291 return print_json(&storage_layout);
292 }
293
294 let headers = vec![
295 Cell::new("Name"),
296 Cell::new("Type"),
297 Cell::new("Slot"),
298 Cell::new("Offset"),
299 Cell::new("Bytes"),
300 Cell::new("Contract"),
301 ];
302
303 print_table(
304 headers,
305 |table| {
306 for slot in &storage_layout.storage {
307 let storage_type = storage_layout.types.get(&slot.storage_type);
308 table.add_row([
309 slot.label.as_str(),
310 storage_type.map_or("?", |t| &t.label),
311 &slot.slot,
312 &slot.offset.to_string(),
313 storage_type.map_or("?", |t| &t.number_of_bytes),
314 &slot.contract,
315 ]);
316 }
317 },
318 should_wrap,
319 )
320}
321
322fn print_method_identifiers(
323 method_identifiers: &Option<BTreeMap<String, String>>,
324 should_wrap: bool,
325) -> Result<()> {
326 let Some(method_identifiers) = method_identifiers else {
327 return Err(missing_error("method identifiers"));
328 };
329
330 if shell::is_json() {
331 return print_json(method_identifiers);
332 }
333
334 let headers = vec![Cell::new("Method"), Cell::new("Identifier")];
335
336 print_table(
337 headers,
338 |table| {
339 for (method, identifier) in method_identifiers {
340 table.add_row([method, identifier]);
341 }
342 },
343 should_wrap,
344 )
345}
346
347fn print_errors_events(map: &Map<String, Value>, is_err: bool, should_wrap: bool) -> Result<()> {
348 if shell::is_json() {
349 return print_json(map);
350 }
351
352 let headers = if is_err {
353 vec![Cell::new("Error"), Cell::new("Selector")]
354 } else {
355 vec![Cell::new("Event"), Cell::new("Topic")]
356 };
357 print_table(
358 headers,
359 |table| {
360 for (method, selector) in map {
361 table.add_row([method, selector.as_str().unwrap()]);
362 }
363 },
364 should_wrap,
365 )
366}
367
368fn print_table(
369 headers: Vec<Cell>,
370 add_rows: impl FnOnce(&mut Table),
371 should_wrap: bool,
372) -> Result<()> {
373 let mut table = Table::new();
374 if shell::is_markdown() {
375 table.load_preset(ASCII_MARKDOWN);
376 } else {
377 table.apply_modifier(UTF8_ROUND_CORNERS);
378 }
379 table.set_header(headers);
380 if should_wrap {
381 table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
382 }
383 add_rows(&mut table);
384 sh_println!("\n{table}\n")?;
385 Ok(())
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
390pub enum ContractArtifactField {
391 Abi,
392 Bytecode,
393 DeployedBytecode,
394 Assembly,
395 AssemblyOptimized,
396 LegacyAssembly,
397 MethodIdentifiers,
398 GasEstimates,
399 StorageLayout,
400 DevDoc,
401 Ir,
402 IrOptimized,
403 Metadata,
404 UserDoc,
405 Ewasm,
406 Errors,
407 Events,
408 StandardJson,
409}
410
411macro_rules! impl_value_enum {
412 (enum $name:ident { $($field:ident => $main:literal $(| $alias:literal)*),+ $(,)? }) => {
413 impl $name {
414 pub const ALL: &'static [Self] = &[$(Self::$field),+];
416
417 pub const fn as_str(&self) -> &'static str {
419 match self {
420 $(
421 Self::$field => $main,
422 )+
423 }
424 }
425
426 pub const fn aliases(&self) -> &'static [&'static str] {
428 match self {
429 $(
430 Self::$field => &[$($alias),*],
431 )+
432 }
433 }
434 }
435
436 impl ::clap::ValueEnum for $name {
437 fn value_variants<'a>() -> &'a [Self] {
438 Self::ALL
439 }
440
441 fn to_possible_value(&self) -> Option<::clap::builder::PossibleValue> {
442 Some(::clap::builder::PossibleValue::new(Self::as_str(self)).aliases(Self::aliases(self)))
443 }
444
445 fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
446 let _ = ignore_case;
447 <Self as ::std::str::FromStr>::from_str(input)
448 }
449 }
450
451 impl ::std::str::FromStr for $name {
452 type Err = String;
453
454 fn from_str(s: &str) -> Result<Self, Self::Err> {
455 match s {
456 $(
457 $main $(| $alias)* => Ok(Self::$field),
458 )+
459 _ => Err(format!(concat!("Invalid ", stringify!($name), " value: {}"), s)),
460 }
461 }
462 }
463 };
464}
465
466impl_value_enum! {
467 enum ContractArtifactField {
468 Abi => "abi",
469 Bytecode => "bytecode" | "bytes" | "b",
470 DeployedBytecode => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode"
471 | "deployed" | "deployedbytecode",
472 Assembly => "assembly" | "asm",
473 LegacyAssembly => "legacyAssembly" | "legacyassembly" | "legacy_assembly",
474 AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized"
475 | "assembly_optimized" | "asmopt" | "assembly-optimized"
476 | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized",
477 MethodIdentifiers => "methodIdentifiers" | "methodidentifiers" | "methods"
478 | "method_identifiers" | "method-identifiers" | "mi",
479 GasEstimates => "gasEstimates" | "gas" | "gas_estimates" | "gas-estimates"
480 | "gasestimates",
481 StorageLayout => "storageLayout" | "storage_layout" | "storage-layout"
482 | "storagelayout" | "storage",
483 DevDoc => "devdoc" | "dev-doc" | "devDoc",
484 Ir => "ir" | "iR" | "IR",
485 IrOptimized => "irOptimized" | "ir-optimized" | "iroptimized" | "iro" | "iropt",
486 Metadata => "metadata" | "meta",
487 UserDoc => "userdoc" | "userDoc" | "user-doc",
488 Ewasm => "ewasm" | "e-wasm",
489 Errors => "errors" | "er",
490 Events => "events" | "ev",
491 StandardJson => "standardJson" | "standard-json" | "standard_json",
492 }
493}
494
495impl TryFrom<ContractArtifactField> for ContractOutputSelection {
496 type Error = eyre::Error;
497
498 fn try_from(field: ContractArtifactField) -> Result<Self, Self::Error> {
499 type Caf = ContractArtifactField;
500 match field {
501 Caf::Abi => Ok(Self::Abi),
502 Caf::Bytecode => {
503 Ok(Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)))
504 }
505 Caf::DeployedBytecode => Ok(Self::Evm(EvmOutputSelection::DeployedByteCode(
506 DeployedBytecodeOutputSelection::All,
507 ))),
508 Caf::Assembly | Caf::AssemblyOptimized => Ok(Self::Evm(EvmOutputSelection::Assembly)),
509 Caf::LegacyAssembly => Ok(Self::Evm(EvmOutputSelection::LegacyAssembly)),
510 Caf::MethodIdentifiers => Ok(Self::Evm(EvmOutputSelection::MethodIdentifiers)),
511 Caf::GasEstimates => Ok(Self::Evm(EvmOutputSelection::GasEstimates)),
512 Caf::StorageLayout => Ok(Self::StorageLayout),
513 Caf::DevDoc => Ok(Self::DevDoc),
514 Caf::Ir => Ok(Self::Ir),
515 Caf::IrOptimized => Ok(Self::IrOptimized),
516 Caf::Metadata => Ok(Self::Metadata),
517 Caf::UserDoc => Ok(Self::UserDoc),
518 Caf::Ewasm => Ok(Self::Ewasm(EwasmOutputSelection::All)),
519 Caf::Errors => Ok(Self::Abi),
520 Caf::Events => Ok(Self::Abi),
521 Caf::StandardJson => {
522 Err(eyre!("StandardJson is not supported for ContractOutputSelection"))
523 }
524 }
525 }
526}
527
528impl PartialEq<ContractOutputSelection> for ContractArtifactField {
529 fn eq(&self, other: &ContractOutputSelection) -> bool {
530 type Cos = ContractOutputSelection;
531 type Eos = EvmOutputSelection;
532 matches!(
533 (self, other),
534 (Self::Abi | Self::Events, Cos::Abi)
535 | (Self::Errors, Cos::Abi)
536 | (Self::Bytecode, Cos::Evm(Eos::ByteCode(_)))
537 | (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_)))
538 | (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly))
539 | (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly))
540 | (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers))
541 | (Self::GasEstimates, Cos::Evm(Eos::GasEstimates))
542 | (Self::StorageLayout, Cos::StorageLayout)
543 | (Self::DevDoc, Cos::DevDoc)
544 | (Self::Ir, Cos::Ir)
545 | (Self::IrOptimized, Cos::IrOptimized)
546 | (Self::Metadata, Cos::Metadata)
547 | (Self::UserDoc, Cos::UserDoc)
548 | (Self::Ewasm, Cos::Ewasm(_))
549 )
550 }
551}
552
553impl fmt::Display for ContractArtifactField {
554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555 f.write_str(self.as_str())
556 }
557}
558
559impl ContractArtifactField {
560 pub const fn can_skip_field(&self) -> bool {
562 matches!(self, Self::Bytecode | Self::DeployedBytecode | Self::StandardJson)
563 }
564}
565
566fn print_json(obj: &impl serde::Serialize) -> Result<()> {
567 sh_println!("{}", serde_json::to_string_pretty(obj)?)?;
568 Ok(())
569}
570
571fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> {
572 sh_println!("{}", get_json_str(obj, key)?)?;
573 Ok(())
574}
575
576fn print_yul(yul: Option<&str>, strip_comments: bool) -> Result<()> {
577 let Some(yul) = yul else {
578 return Err(missing_error("IR output"));
579 };
580
581 static YUL_COMMENTS: LazyLock<Regex> =
582 LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*?\*/)").unwrap());
583
584 if strip_comments {
585 sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?;
586 } else {
587 sh_println!("{yul}")?;
588 }
589
590 Ok(())
591}
592
593fn get_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<String> {
594 let value = serde_json::to_value(obj)?;
595 let value = if let Some(key) = key
596 && let Some(value) = value.get(key)
597 {
598 value
599 } else {
600 &value
601 };
602 Ok(match value.as_str() {
603 Some(s) => s.to_string(),
604 None => format!("{value:#}"),
605 })
606}
607
608fn missing_error(field: &str) -> eyre::Error {
609 eyre!(
610 "{field} missing from artifact; \
611 this could be a spurious caching issue, consider running `forge clean`"
612 )
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn contract_output_selection() {
621 for &field in ContractArtifactField::ALL {
622 if field == ContractArtifactField::StandardJson {
623 let selection: Result<ContractOutputSelection, _> = field.try_into();
624 assert!(
625 selection
626 .unwrap_err()
627 .to_string()
628 .eq("StandardJson is not supported for ContractOutputSelection")
629 );
630 } else {
631 let selection: ContractOutputSelection = field.try_into().unwrap();
632 assert_eq!(field, selection);
633
634 let s = field.as_str();
635 assert_eq!(s, field.to_string());
636 assert_eq!(s.parse::<ContractArtifactField>().unwrap(), field);
637 for alias in field.aliases() {
638 assert_eq!(alias.parse::<ContractArtifactField>().unwrap(), field);
639 }
640 }
641 }
642 }
643}