diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index d1836c9bccf9..42ba370b2edc 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -1,3 +1,4 @@ +use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param}; use alloy_primitives::{hex, keccak256, Address}; use clap::Parser; use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Cell, Table}; @@ -17,7 +18,8 @@ use foundry_compilers::{ utils::canonicalize, }; use regex::Regex; -use std::{fmt, sync::LazyLock}; +use serde_json::{Map, Value}; +use std::{collections::BTreeMap, fmt, sync::LazyLock}; /// CLI arguments for `forge inspect`. #[derive(Clone, Debug, Parser)] @@ -29,10 +31,6 @@ pub struct InspectArgs { #[arg(value_enum)] pub field: ContractArtifactField, - /// Pretty print the selected field, if supported. - #[arg(long)] - pub pretty: bool, - /// All build arguments are supported #[command(flatten)] build: BuildOpts, @@ -40,7 +38,7 @@ pub struct InspectArgs { impl InspectArgs { pub fn run(self) -> Result<()> { - let Self { contract, field, build, pretty } = self; + let Self { contract, field, build } = self; trace!(target: "forge", ?field, ?contract, "running forge inspect"); @@ -85,12 +83,7 @@ impl InspectArgs { .abi .as_ref() .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?; - if pretty { - let source = foundry_cli::utils::abi_to_solidity(abi, &contract.name)?; - sh_println!("{source}")?; - } else { - print_json(abi)?; - } + print_abi(abi)?; } ContractArtifactField::Bytecode => { print_json_str(&artifact.bytecode, Some("object"))?; @@ -105,7 +98,7 @@ impl InspectArgs { print_json_str(&artifact.legacy_assembly, None)?; } ContractArtifactField::MethodIdentifiers => { - print_json(&artifact.method_identifiers)?; + print_method_identifiers(&artifact.method_identifiers)?; } ContractArtifactField::GasEstimates => { print_json(&artifact.gas_estimates)?; @@ -117,10 +110,10 @@ impl InspectArgs { print_json(&artifact.devdoc)?; } ContractArtifactField::Ir => { - print_yul(artifact.ir.as_deref(), self.pretty)?; + print_yul(artifact.ir.as_deref())?; } ContractArtifactField::IrOptimized => { - print_yul(artifact.ir_optimized.as_deref(), self.pretty)?; + print_yul(artifact.ir_optimized.as_deref())?; } ContractArtifactField::Metadata => { print_json(&artifact.metadata)?; @@ -132,37 +125,12 @@ impl InspectArgs { print_json_str(&artifact.ewasm, None)?; } ContractArtifactField::Errors => { - let mut out = serde_json::Map::new(); - if let Some(abi) = &artifact.abi { - let abi = &abi; - // Print the signature of all errors. - for er in abi.errors.iter().flat_map(|(_, errors)| errors) { - let types = er.inputs.iter().map(|p| p.ty.clone()).collect::<Vec<_>>(); - let sig = format!("{:x}", er.selector()); - let sig_trimmed = &sig[0..8]; - out.insert( - format!("{}({})", er.name, types.join(",")), - sig_trimmed.to_string().into(), - ); - } - } - print_json(&out)?; + let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors); + print_errors_events(&out, true)?; } ContractArtifactField::Events => { - let mut out = serde_json::Map::new(); - if let Some(abi) = &artifact.abi { - let abi = &abi; - // Print the topic of all events including anonymous. - for ev in abi.events.iter().flat_map(|(_, events)| events) { - let types = ev.inputs.iter().map(|p| p.ty.clone()).collect::<Vec<_>>(); - let topic = hex::encode(keccak256(ev.signature())); - out.insert( - format!("{}({})", ev.name, types.join(",")), - format!("0x{topic}").into(), - ); - } - } - print_json(&out)?; + let out = artifact.abi.as_ref().map_or(Map::new(), parse_events); + print_errors_events(&out, false)?; } ContractArtifactField::Eof => { print_eof(artifact.deployed_bytecode.and_then(|b| b.bytecode))?; @@ -176,6 +144,127 @@ impl InspectArgs { } } +fn parse_errors(abi: &JsonAbi) -> Map<String, Value> { + let mut out = serde_json::Map::new(); + for er in abi.errors.iter().flat_map(|(_, errors)| errors) { + let types = get_ty_sig(&er.inputs); + let sig = format!("{:x}", er.selector()); + let sig_trimmed = &sig[0..8]; + out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into()); + } + out +} + +fn parse_events(abi: &JsonAbi) -> Map<String, Value> { + let mut out = serde_json::Map::new(); + for ev in abi.events.iter().flat_map(|(_, events)| events) { + let types = parse_event_params(&ev.inputs); + let topic = hex::encode(keccak256(ev.signature())); + out.insert(format!("{}({})", ev.name, types), format!("0x{topic}").into()); + } + out +} + +fn parse_event_params(ev_params: &[EventParam]) -> String { + ev_params + .iter() + .map(|p| { + if let Some(ty) = p.internal_type() { + return internal_ty(ty) + } + p.ty.clone() + }) + .collect::<Vec<_>>() + .join(",") +} + +fn print_abi(abi: &JsonAbi) -> Result<()> { + if shell::is_json() { + return print_json(abi) + } + + let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")]; + print_table(headers, |table| { + // Print events + for ev in abi.events.iter().flat_map(|(_, events)| events) { + let types = parse_event_params(&ev.inputs); + let selector = ev.selector().to_string(); + table.add_row(["event", &format!("{}({})", ev.name, types), &selector]); + } + + // Print errors + for er in abi.errors.iter().flat_map(|(_, errors)| errors) { + let selector = er.selector().to_string(); + table.add_row([ + "error", + &format!("{}({})", er.name, get_ty_sig(&er.inputs)), + &selector, + ]); + } + + // Print functions + for func in abi.functions.iter().flat_map(|(_, f)| f) { + let selector = func.selector().to_string(); + let state_mut = func.state_mutability.as_json_str(); + let func_sig = if !func.outputs.is_empty() { + format!( + "{}({}) {state_mut} returns ({})", + func.name, + get_ty_sig(&func.inputs), + get_ty_sig(&func.outputs) + ) + } else { + format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs)) + }; + table.add_row(["function", &func_sig, &selector]); + } + + if let Some(constructor) = abi.constructor() { + let state_mut = constructor.state_mutability.as_json_str(); + table.add_row([ + "constructor", + &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)), + "", + ]); + } + + if let Some(fallback) = &abi.fallback { + let state_mut = fallback.state_mutability.as_json_str(); + table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]); + } + + if let Some(receive) = &abi.receive { + let state_mut = receive.state_mutability.as_json_str(); + table.add_row(["receive", &format!("receive() {state_mut}"), ""]); + } + }) +} + +fn get_ty_sig(inputs: &[Param]) -> String { + inputs + .iter() + .map(|p| { + if let Some(ty) = p.internal_type() { + return internal_ty(ty); + } + p.ty.clone() + }) + .collect::<Vec<_>>() + .join(",") +} + +fn internal_ty(ty: &InternalType) -> String { + let contract_ty = + |c: &Option<String>, ty: &String| c.clone().map_or(ty.clone(), |c| format!("{c}.{ty}")); + match ty { + InternalType::AddressPayable(addr) => addr.clone(), + InternalType::Contract(contract) => contract.clone(), + InternalType::Enum { contract, ty } => contract_ty(contract, ty), + InternalType::Struct { contract, ty } => contract_ty(contract, ty), + InternalType::Other { contract, ty } => contract_ty(contract, ty), + } +} + pub fn print_storage_layout(storage_layout: Option<&StorageLayout>) -> Result<()> { let Some(storage_layout) = storage_layout else { eyre::bail!("Could not get storage layout"); @@ -185,30 +274,70 @@ pub fn print_storage_layout(storage_layout: Option<&StorageLayout>) -> Result<() return print_json(&storage_layout) } - let mut table = Table::new(); - table.apply_modifier(UTF8_ROUND_CORNERS); - - table.set_header(vec![ + let headers = vec![ Cell::new("Name"), Cell::new("Type"), Cell::new("Slot"), Cell::new("Offset"), Cell::new("Bytes"), Cell::new("Contract"), - ]); - - for slot in &storage_layout.storage { - let storage_type = storage_layout.types.get(&slot.storage_type); - table.add_row([ - slot.label.as_str(), - storage_type.map_or("?", |t| &t.label), - &slot.slot, - &slot.offset.to_string(), - storage_type.map_or("?", |t| &t.number_of_bytes), - &slot.contract, - ]); + ]; + + print_table(headers, |table| { + for slot in &storage_layout.storage { + let storage_type = storage_layout.types.get(&slot.storage_type); + table.add_row([ + slot.label.as_str(), + storage_type.map_or("?", |t| &t.label), + &slot.slot, + &slot.offset.to_string(), + storage_type.map_or("?", |t| &t.number_of_bytes), + &slot.contract, + ]); + } + }) +} + +fn print_method_identifiers(method_identifiers: &Option<BTreeMap<String, String>>) -> Result<()> { + let Some(method_identifiers) = method_identifiers else { + eyre::bail!("Could not get method identifiers"); + }; + + if shell::is_json() { + return print_json(method_identifiers) + } + + let headers = vec![Cell::new("Method"), Cell::new("Identifier")]; + + print_table(headers, |table| { + for (method, identifier) in method_identifiers { + table.add_row([method, identifier]); + } + }) +} + +fn print_errors_events(map: &Map<String, Value>, is_err: bool) -> Result<()> { + if shell::is_json() { + return print_json(map); } + let headers = if is_err { + vec![Cell::new("Error"), Cell::new("Selector")] + } else { + vec![Cell::new("Event"), Cell::new("Topic")] + }; + print_table(headers, |table| { + for (method, selector) in map { + table.add_row([method, selector.as_str().unwrap()]); + } + }) +} + +fn print_table(headers: Vec<Cell>, add_rows: impl FnOnce(&mut Table)) -> Result<()> { + let mut table = Table::new(); + table.apply_modifier(UTF8_ROUND_CORNERS); + table.set_header(headers); + add_rows(&mut table); sh_println!("\n{table}\n")?; Ok(()) } @@ -407,7 +536,7 @@ fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> Ok(()) } -fn print_yul(yul: Option<&str>, pretty: bool) -> Result<()> { +fn print_yul(yul: Option<&str>) -> Result<()> { let Some(yul) = yul else { eyre::bail!("Could not get IR output"); }; @@ -415,11 +544,7 @@ fn print_yul(yul: Option<&str>, pretty: bool) -> Result<()> { static YUL_COMMENTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*\*/)").unwrap()); - if pretty { - sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?; - } else { - sh_println!("{yul}")?; - } + sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?; Ok(()) } diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index f0774a36802a..35a7bd4411a7 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3230,19 +3230,177 @@ Compiler run successful! .stdout_eq(str![[r#""{...}""#]].is_json()); }); -// <https://github.com/foundry-rs/foundry/issues/6816> forgetest_init!(can_inspect_counter_pretty, |prj, cmd| { - cmd.args(["inspect", "src/Counter.sol:Counter", "abi", "--pretty"]).assert_success().stdout_eq( - str![[r#" -interface Counter { - function increment() external; - function number() external view returns (uint256); - function setNumber(uint256 newNumber) external; + cmd.args(["inspect", "src/Counter.sol:Counter", "abi"]).assert_success().stdout_eq(str![[r#" + +╭----------+---------------------------------+------------╮ +| Type | Signature | Selector | ++=========================================================+ +| function | increment() nonpayable | 0xd09de08a | +|----------+---------------------------------+------------| +| function | number() view returns (uint256) | 0x8381f58a | +|----------+---------------------------------+------------| +| function | setNumber(uint256) nonpayable | 0x3fb5c1cb | +╰----------+---------------------------------+------------╯ + + +"#]]); +}); + +const CUSTOM_COUNTER: &str = r#" + contract Counter { + uint256 public number; + uint64 public count; + struct MyStruct { + uint64 count; + } + struct ErrWithMsg { + string message; + } + + event Incremented(uint256 newValue); + event Decremented(uint256 newValue); + + error NumberIsZero(); + error CustomErr(ErrWithMsg e); + + constructor(uint256 _number) { + number = _number; + } + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() external { + number++; + } + + function decrement() public payable { + if (number == 0) { + return; + } + number--; + } + + function square() public { + number = number * number; + } + + fallback() external payable { + ErrWithMsg memory err = ErrWithMsg("Fallback function is not allowed"); + revert CustomErr(err); + } + + receive() external payable { + count++; + } + + function setStruct(MyStruct memory s, uint32 b) public { + count = s.count; + } } + "#; +forgetest!(inspect_custom_counter_abi, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER).unwrap(); + + cmd.args(["inspect", "Counter", "abi"]).assert_success().stdout_eq(str![[r#" + +╭-------------+-----------------------------------------------+--------------------------------------------------------------------╮ +| Type | Signature | Selector | ++==================================================================================================================================+ +| event | Decremented(uint256) | 0xc9118d86370931e39644ee137c931308fa3774f6c90ab057f0c3febf427ef94a | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| event | Incremented(uint256) | 0x20d8a6f5a693f9d1d627a598e8820f7a55ee74c183aa8f1a30e8d4e8dd9a8d84 | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| error | CustomErr(Counter.ErrWithMsg) | 0x0625625a | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| error | NumberIsZero() | 0xde5d32ac | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | count() view returns (uint64) | 0x06661abd | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | decrement() payable | 0x2baeceb7 | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | increment() nonpayable | 0xd09de08a | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | number() view returns (uint256) | 0x8381f58a | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | setNumber(uint256) nonpayable | 0x3fb5c1cb | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | setStruct(Counter.MyStruct,uint32) nonpayable | 0x08ef7366 | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| function | square() nonpayable | 0xd742cb01 | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| constructor | constructor(uint256) nonpayable | | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| fallback | fallback() payable | | +|-------------+-----------------------------------------------+--------------------------------------------------------------------| +| receive | receive() payable | | +╰-------------+-----------------------------------------------+--------------------------------------------------------------------╯ -"#]], - ); +"#]]); +}); + +forgetest!(inspect_custom_counter_events, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER).unwrap(); + + cmd.args(["inspect", "Counter", "events"]).assert_success().stdout_eq(str![[r#" + +╭----------------------+--------------------------------------------------------------------╮ +| Event | Topic | ++===========================================================================================+ +| Decremented(uint256) | 0xc9118d86370931e39644ee137c931308fa3774f6c90ab057f0c3febf427ef94a | +|----------------------+--------------------------------------------------------------------| +| Incremented(uint256) | 0x20d8a6f5a693f9d1d627a598e8820f7a55ee74c183aa8f1a30e8d4e8dd9a8d84 | +╰----------------------+--------------------------------------------------------------------╯ + + +"#]]); +}); + +forgetest!(inspect_custom_counter_errors, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER).unwrap(); + + cmd.args(["inspect", "Counter", "errors"]).assert_success().stdout_eq(str![[r#" + +╭-------------------------------+----------╮ +| Error | Selector | ++==========================================+ +| CustomErr(Counter.ErrWithMsg) | 0625625a | +|-------------------------------+----------| +| NumberIsZero() | de5d32ac | +╰-------------------------------+----------╯ + + +"#]]); +}); + +forgetest!(inspect_custom_counter_method_identifiers, |prj, cmd| { + prj.add_source("Counter.sol", CUSTOM_COUNTER).unwrap(); + + cmd.args(["inspect", "Counter", "method-identifiers"]).assert_success().stdout_eq(str![[r#" + +╭----------------------------+------------╮ +| Method | Identifier | ++=========================================+ +| count() | 06661abd | +|----------------------------+------------| +| decrement() | 2baeceb7 | +|----------------------------+------------| +| increment() | d09de08a | +|----------------------------+------------| +| number() | 8381f58a | +|----------------------------+------------| +| setNumber(uint256) | 3fb5c1cb | +|----------------------------+------------| +| setStruct((uint64),uint32) | 08ef7366 | +|----------------------------+------------| +| square() | d742cb01 | +╰----------------------------+------------╯ + + +"#]]); }); // checks that `clean` also works with the "out" value set in Config