forge/cmd/
inspect.rs

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/// 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.can_skip_field() && !cos.iter().any(|selected| field == *selected) {
54            cos.push(field.try_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        // Get the solc version if specified
65        let solc_version = build.use_solc.clone();
66
67        // Build modified Args
68        let modified_build_args = BuildOpts {
69            compiler: CompilerOpts { extra_output: cos, optimize: optimized, ..build.compiler },
70            ..build
71        };
72
73        // Build the project
74        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        // Find the artifact
80        let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
81
82        // Match on ContractArtifactFields and pretty-print
83        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        // Print events
198        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        // Print errors
205        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        // Print functions
215        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/// Contract level output selection
355#[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            /// All the variants of this enum.
381            pub const ALL: &'static [Self] = &[$(Self::$field),+];
382
383            /// Returns the string representation of `self`.
384            #[inline]
385            pub const fn as_str(&self) -> &'static str {
386                match self {
387                    $(
388                        Self::$field => $main,
389                    )+
390                }
391            }
392
393            /// Returns all the aliases of `self`.
394            #[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    /// Returns true if this field does not need to be passed to the compiler.
532    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}