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