forge/cmd/
inspect.rs

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