Skip to main content

forge/cmd/
inspect.rs

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