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