Skip to main content

forge/cmd/
inspect.rs

1use alloy_json_abi::{Event, EventParam, InternalType, JsonAbi, Param};
2use clap::Parser;
3use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
4use eyre::{Result, eyre};
5use foundry_cli::opts::{BuildOpts, CompilerOpts};
6use foundry_common::{
7    compile::{PathOrContractInfo, ProjectCompiler},
8    find_matching_contract_artifact, find_target_path, shell,
9};
10use foundry_compilers::{
11    ProjectCompileOutput,
12    artifacts::{
13        StorageLayout,
14        output_selection::{
15            BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
16            EvmOutputSelection, EwasmOutputSelection,
17        },
18    },
19    solc::SolcLanguage,
20};
21use path_slash::PathExt;
22use regex::Regex;
23use serde_json::{Map, Value};
24use solar::sema::interface::source_map::FileName;
25use std::{collections::BTreeMap, fmt, ops::ControlFlow, path::Path, str::FromStr, sync::LazyLock};
26
27/// CLI arguments for `forge inspect`.
28#[derive(Clone, Debug, Parser)]
29pub struct InspectArgs {
30    /// The identifier of the contract to inspect in the form `(<path>:)?<contractname>`.
31    #[arg(value_parser = PathOrContractInfo::from_str)]
32    pub contract: PathOrContractInfo,
33
34    /// The contract artifact field to inspect.
35    #[arg(value_enum)]
36    pub field: ContractArtifactField,
37
38    /// All build arguments are supported
39    #[command(flatten)]
40    build: BuildOpts,
41
42    /// Whether to remove comments when inspecting `ir` and `irOptimized` artifact fields.
43    #[arg(long, short, help_heading = "Display options")]
44    pub strip_yul_comments: bool,
45
46    /// Whether to wrap the table to the terminal width.
47    #[arg(long, short, help_heading = "Display options")]
48    pub wrap: bool,
49}
50
51impl InspectArgs {
52    pub fn run(self) -> Result<()> {
53        let Self { contract, field, build, strip_yul_comments, wrap } = self;
54
55        trace!(target: "forge", ?field, ?contract, "running forge inspect");
56
57        // Map field to ContractOutputSelection
58        let mut cos = build.compiler.extra_output;
59        if !field.can_skip_field() && !cos.iter().any(|selected| field == *selected) {
60            cos.push(field.try_into()?);
61        }
62
63        // Run Optimized?
64        let optimized = if field == ContractArtifactField::AssemblyOptimized {
65            Some(true)
66        } else {
67            build.compiler.optimize
68        };
69
70        // Get the solc version if specified
71        let solc_version = build.use_solc.clone();
72
73        // Build modified Args
74        let modified_build_args = BuildOpts {
75            compiler: CompilerOpts { extra_output: cos, optimize: optimized, ..build.compiler },
76            ..build
77        };
78
79        // Build the project
80        let project = modified_build_args.project()?;
81        let target_path = find_target_path(&project, &contract)?;
82        if field == ContractArtifactField::Linearization && !is_solidity_source(&target_path) {
83            eyre::bail!(
84                "linearization inspection is only supported for Solidity contracts (.sol targets)"
85            );
86        }
87        let compiler = ProjectCompiler::new().quiet(true);
88        let mut output = compiler.files([target_path.clone()]).compile(&project)?;
89
90        // Find the artifact
91        let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
92
93        // Match on ContractArtifactFields and pretty-print
94        match field {
95            ContractArtifactField::Abi => {
96                let abi = artifact.abi.as_ref().ok_or_else(|| missing_error("ABI"))?;
97                print_abi(abi, wrap)?;
98            }
99            ContractArtifactField::Bytecode => {
100                print_json_str(&artifact.bytecode, Some("object"))?;
101            }
102            ContractArtifactField::DeployedBytecode => {
103                print_json_str(&artifact.deployed_bytecode, Some("object"))?;
104            }
105            ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => {
106                print_json_str(&artifact.assembly, None)?;
107            }
108            ContractArtifactField::LegacyAssembly => {
109                print_json_str(&artifact.legacy_assembly, None)?;
110            }
111            ContractArtifactField::MethodIdentifiers => {
112                print_method_identifiers(&artifact.method_identifiers, wrap)?;
113            }
114            ContractArtifactField::GasEstimates => {
115                print_json(&artifact.gas_estimates)?;
116            }
117            ContractArtifactField::StorageLayout => {
118                print_storage_layout(artifact.storage_layout.as_ref(), wrap)?;
119            }
120            ContractArtifactField::DevDoc => {
121                print_json(&artifact.devdoc)?;
122            }
123            ContractArtifactField::Ir => {
124                print_yul(artifact.ir.as_deref(), strip_yul_comments)?;
125            }
126            ContractArtifactField::IrOptimized => {
127                print_yul(artifact.ir_optimized.as_deref(), strip_yul_comments)?;
128            }
129            ContractArtifactField::Metadata => {
130                print_json(&artifact.metadata)?;
131            }
132            ContractArtifactField::UserDoc => {
133                print_json(&artifact.userdoc)?;
134            }
135            ContractArtifactField::Ewasm => {
136                print_json_str(&artifact.ewasm, None)?;
137            }
138            ContractArtifactField::Errors => {
139                let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors);
140                print_errors_events(&out, true, wrap)?;
141            }
142            ContractArtifactField::Events => {
143                let out = artifact.abi.as_ref().map_or(Map::new(), parse_events);
144                print_errors_events(&out, false, wrap)?;
145            }
146            ContractArtifactField::StandardJson => {
147                let standard_json = if let Some(version) = solc_version {
148                    let version = version.parse()?;
149                    let mut standard_json =
150                        project.standard_json_input(&target_path)?.normalize_evm_version(&version);
151                    standard_json.settings.sanitize(&version, SolcLanguage::Solidity);
152                    standard_json
153                } else {
154                    project.standard_json_input(&target_path)?
155                };
156                print_json(&standard_json)?;
157            }
158            ContractArtifactField::Libraries => {
159                let all_libs: Vec<String> = artifact
160                    .all_link_references()
161                    .into_iter()
162                    .flat_map(|(path, libs)| {
163                        libs.into_keys().map(move |lib| format!("{path}:{lib}"))
164                    })
165                    .collect();
166                if shell::is_json() {
167                    return print_json(&all_libs);
168                }
169                sh_status!("Dynamically linked libraries:")?;
170                for lib in &all_libs {
171                    sh_println!("{lib}")?;
172                }
173            }
174            ContractArtifactField::Linearization => {
175                print_linearization(
176                    &mut output,
177                    project.root(),
178                    &target_path,
179                    contract.name(),
180                    wrap,
181                )?;
182            }
183        };
184
185        Ok(())
186    }
187}
188
189fn parse_errors(abi: &JsonAbi) -> Map<String, Value> {
190    let mut out = serde_json::Map::new();
191    for er in abi.errors.values().flatten() {
192        let types = get_ty_sig(&er.inputs);
193        let sig = format!("{:x}", er.selector());
194        let sig_trimmed = &sig[0..8];
195        out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into());
196    }
197    out
198}
199
200fn parse_events(abi: &JsonAbi) -> Map<String, Value> {
201    let mut out = serde_json::Map::new();
202    for ev in abi.events.values().flatten() {
203        let types = parse_event_params(&ev.inputs);
204        let topic = event_topic(ev).map_or(Value::Null, Into::into);
205        out.insert(format!("{}({})", ev.name, types), topic);
206    }
207    out
208}
209
210/// Returns topic0 for non-anonymous events. Anonymous events have no signature topic.
211fn event_topic(ev: &Event) -> Option<String> {
212    (!ev.anonymous).then(|| ev.selector().to_string())
213}
214
215fn parse_event_params(ev_params: &[EventParam]) -> String {
216    ev_params
217        .iter()
218        .map(|p| {
219            if let Some(ty) = p.internal_type() {
220                return internal_ty(ty);
221            }
222            p.ty.clone()
223        })
224        .collect::<Vec<_>>()
225        .join(",")
226}
227
228fn print_abi(abi: &JsonAbi, should_wrap: bool) -> Result<()> {
229    if shell::is_json() {
230        return print_json(abi);
231    }
232
233    let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")];
234    print_table(
235        headers,
236        |table| {
237            // Print events
238            for ev in abi.events.values().flatten() {
239                let types = parse_event_params(&ev.inputs);
240                let signature = if ev.anonymous {
241                    format!("{}({}) anonymous", ev.name, types)
242                } else {
243                    format!("{}({})", ev.name, types)
244                };
245                let selector = event_topic(ev).unwrap_or_default();
246                table.add_row(["event", &signature, &selector]);
247            }
248
249            // Print errors
250            for er in abi.errors.values().flatten() {
251                let selector = er.selector().to_string();
252                table.add_row([
253                    "error",
254                    &format!("{}({})", er.name, get_ty_sig(&er.inputs)),
255                    &selector,
256                ]);
257            }
258
259            // Print functions
260            for func in abi.functions.values().flatten() {
261                let selector = func.selector().to_string();
262                let state_mut = func.state_mutability.as_json_str();
263                let func_sig = if func.outputs.is_empty() {
264                    format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs))
265                } else {
266                    format!(
267                        "{}({}) {state_mut} returns ({})",
268                        func.name,
269                        get_ty_sig(&func.inputs),
270                        get_ty_sig(&func.outputs)
271                    )
272                };
273                table.add_row(["function", &func_sig, &selector]);
274            }
275
276            if let Some(constructor) = abi.constructor() {
277                let state_mut = constructor.state_mutability.as_json_str();
278                table.add_row([
279                    "constructor",
280                    &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)),
281                    "",
282                ]);
283            }
284
285            if let Some(fallback) = &abi.fallback {
286                let state_mut = fallback.state_mutability.as_json_str();
287                table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]);
288            }
289
290            if let Some(receive) = &abi.receive {
291                let state_mut = receive.state_mutability.as_json_str();
292                table.add_row(["receive", &format!("receive() {state_mut}"), ""]);
293            }
294        },
295        should_wrap,
296    )
297}
298
299fn get_ty_sig(inputs: &[Param]) -> String {
300    inputs
301        .iter()
302        .map(|p| {
303            if let Some(ty) = p.internal_type() {
304                return internal_ty(ty);
305            }
306            p.ty.clone()
307        })
308        .collect::<Vec<_>>()
309        .join(",")
310}
311
312fn internal_ty(ty: &InternalType) -> String {
313    let contract_ty =
314        |c: Option<&str>, ty: &String| c.map_or_else(|| ty.clone(), |c| format!("{c}.{ty}"));
315    match ty {
316        InternalType::AddressPayable(addr) => addr.clone(),
317        InternalType::Contract(contract) => contract.clone(),
318        InternalType::Enum { contract, ty } => contract_ty(contract.as_deref(), ty),
319        InternalType::Struct { contract, ty } => contract_ty(contract.as_deref(), ty),
320        InternalType::Other { contract, ty } => contract_ty(contract.as_deref(), ty),
321    }
322}
323
324pub fn print_storage_layout(
325    storage_layout: Option<&StorageLayout>,
326    should_wrap: bool,
327) -> Result<()> {
328    let Some(storage_layout) = storage_layout else {
329        return Err(missing_error("storage layout"));
330    };
331
332    if shell::is_json() {
333        return print_json(&storage_layout);
334    }
335
336    let headers = vec![
337        Cell::new("Name"),
338        Cell::new("Type"),
339        Cell::new("Slot"),
340        Cell::new("Offset"),
341        Cell::new("Bytes"),
342        Cell::new("Contract"),
343    ];
344
345    print_table(
346        headers,
347        |table| {
348            for slot in &storage_layout.storage {
349                let storage_type = storage_layout.types.get(&slot.storage_type);
350                table.add_row([
351                    slot.label.as_str(),
352                    storage_type.map_or("?", |t| &t.label),
353                    &slot.slot,
354                    &slot.offset.to_string(),
355                    storage_type.map_or("?", |t| &t.number_of_bytes),
356                    &slot.contract,
357                ]);
358            }
359        },
360        should_wrap,
361    )
362}
363
364fn print_method_identifiers(
365    method_identifiers: &Option<BTreeMap<String, String>>,
366    should_wrap: bool,
367) -> Result<()> {
368    let Some(method_identifiers) = method_identifiers else {
369        return Err(missing_error("method identifiers"));
370    };
371
372    if shell::is_json() {
373        return print_json(method_identifiers);
374    }
375
376    let headers = vec![Cell::new("Method"), Cell::new("Identifier")];
377
378    print_table(
379        headers,
380        |table| {
381            for (method, identifier) in method_identifiers {
382                table.add_row([method.as_str(), identifier.as_str()]);
383            }
384        },
385        should_wrap,
386    )
387}
388
389fn print_errors_events(map: &Map<String, Value>, is_err: bool, should_wrap: bool) -> Result<()> {
390    if shell::is_json() {
391        return print_json(map);
392    }
393
394    let headers = if is_err {
395        vec![Cell::new("Error"), Cell::new("Selector")]
396    } else {
397        vec![Cell::new("Event"), Cell::new("Topic")]
398    };
399    print_table(
400        headers,
401        |table| {
402            for (method, selector) in map {
403                table.add_row([method.as_str(), selector.as_str().unwrap_or("")]);
404            }
405        },
406        should_wrap,
407    )
408}
409
410fn print_table(
411    headers: Vec<Cell>,
412    add_rows: impl FnOnce(&mut Table),
413    should_wrap: bool,
414) -> Result<()> {
415    let mut table = Table::new();
416    if shell::is_markdown() {
417        table.load_preset(ASCII_MARKDOWN);
418    } else {
419        table.apply_modifier(UTF8_ROUND_CORNERS);
420    }
421    table.set_header(headers);
422    if should_wrap {
423        table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
424    }
425    add_rows(&mut table);
426    sh_println!("\n{table}\n")?;
427    Ok(())
428}
429
430fn print_linearization(
431    output: &mut ProjectCompileOutput,
432    root: &Path,
433    target_path: &Path,
434    target_name: Option<&str>,
435    should_wrap: bool,
436) -> Result<()> {
437    let mut chain = Vec::new();
438    let mut lowered = false;
439    let compiler = output.parser_mut().solc_mut().compiler_mut();
440    compiler.enter_mut(|compiler| -> Result<()> {
441        let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else { return Ok(()) };
442        lowered = true;
443
444        let hir = &compiler.gcx().hir;
445        let matching_contracts = hir
446            .contract_ids()
447            .filter(|id| {
448                let contract = hir.contract(*id);
449                if let Some(target_name) = target_name
450                    && contract.name.as_str() != target_name
451                {
452                    return false;
453                }
454
455                matches!(
456                    &hir.source(contract.source).file.name,
457                    FileName::Real(path) if path == target_path
458                )
459            })
460            .collect::<Vec<_>>();
461
462        let target_contract = match matching_contracts.as_slice() {
463            [id] => *id,
464            [] => {
465                if let Some(target_name) = target_name {
466                    eyre::bail!(
467                        "Could not find contract `{target_name}` in `{}`",
468                        target_path.display()
469                    );
470                }
471                eyre::bail!("Could not find contract in `{}`", target_path.display());
472            }
473            _ => {
474                eyre::bail!(
475                    "Multiple contracts found in the same file, please specify the target <path>:<contract> or <contract>"
476                );
477            }
478        };
479
480        for (order, base_id) in hir.contract(target_contract).linearized_bases.iter().enumerate() {
481            let contract = hir.contract(*base_id);
482            let source = hir.source(contract.source);
483            let FileName::Real(path) = &source.file.name else { continue };
484            let path = path.strip_prefix(root).unwrap_or(path);
485            chain.push((
486                order,
487                path.to_slash_lossy().into_owned(),
488                contract.name.as_str().to_string(),
489            ));
490        }
491
492        Ok(())
493    })?;
494
495    // `compiler.sess()` inside of `ProjectCompileOutput` is built with `with_buffer_emitter`.
496    let diags = compiler.sess().dcx.emitted_diagnostics().unwrap();
497    if compiler.sess().dcx.has_errors().is_err() {
498        eyre::bail!("{diags}");
499    } else {
500        let _ = sh_eprint!("{diags}");
501    }
502    if !lowered {
503        eyre::bail!(
504            "unable to inspect linearization: failed to lower Solidity ASTs for `{}`",
505            target_path.display()
506        );
507    }
508
509    if shell::is_json() {
510        let contracts = chain
511            .into_iter()
512            .map(|(order, source, contract)| {
513                serde_json::json!({
514                    "order": order,
515                    "source": source,
516                    "contract": contract,
517                })
518            })
519            .collect::<Vec<_>>();
520        return print_json(&contracts);
521    }
522
523    let headers = vec![Cell::new("Order"), Cell::new("Source"), Cell::new("Contract")];
524    print_table(
525        headers,
526        |table| {
527            for (order, source, contract) in &chain {
528                table.add_row([order.to_string(), source.clone(), contract.clone()]);
529            }
530        },
531        should_wrap,
532    )
533}
534
535/// Contract level output selection
536#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
537pub enum ContractArtifactField {
538    Abi,
539    Bytecode,
540    DeployedBytecode,
541    Assembly,
542    AssemblyOptimized,
543    LegacyAssembly,
544    MethodIdentifiers,
545    GasEstimates,
546    StorageLayout,
547    DevDoc,
548    Ir,
549    IrOptimized,
550    Metadata,
551    UserDoc,
552    Ewasm,
553    Errors,
554    Events,
555    StandardJson,
556    Libraries,
557    Linearization,
558}
559
560macro_rules! impl_value_enum {
561    (enum $name:ident { $($field:ident => $main:literal $(| $alias:literal)*),+ $(,)? }) => {
562        impl $name {
563            /// All the variants of this enum.
564            pub const ALL: &'static [Self] = &[$(Self::$field),+];
565
566            /// Returns the string representation of `self`.
567            pub const fn as_str(&self) -> &'static str {
568                match self {
569                    $(
570                        Self::$field => $main,
571                    )+
572                }
573            }
574
575            /// Returns all the aliases of `self`.
576            pub const fn aliases(&self) -> &'static [&'static str] {
577                match self {
578                    $(
579                        Self::$field => &[$($alias),*],
580                    )+
581                }
582            }
583        }
584
585        impl ::clap::ValueEnum for $name {
586            fn value_variants<'a>() -> &'a [Self] {
587                Self::ALL
588            }
589
590            fn to_possible_value(&self) -> Option<::clap::builder::PossibleValue> {
591                Some(::clap::builder::PossibleValue::new(Self::as_str(self)).aliases(Self::aliases(self)))
592            }
593
594            fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
595                let _ = ignore_case;
596                <Self as ::std::str::FromStr>::from_str(input)
597            }
598        }
599
600        impl ::std::str::FromStr for $name {
601            type Err = String;
602
603            fn from_str(s: &str) -> Result<Self, Self::Err> {
604                match s {
605                    $(
606                        $main $(| $alias)* => Ok(Self::$field),
607                    )+
608                    _ => Err(format!(concat!("Invalid ", stringify!($name), " value: {}"), s)),
609                }
610            }
611        }
612    };
613}
614
615impl_value_enum! {
616    enum ContractArtifactField {
617        Abi               => "abi",
618        Bytecode          => "bytecode" | "bytes" | "b",
619        DeployedBytecode  => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode"
620                             | "deployed" | "deployedbytecode",
621        Assembly          => "assembly" | "asm",
622        LegacyAssembly    => "legacyAssembly" | "legacyassembly" | "legacy_assembly",
623        AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized"
624                             | "assembly_optimized" | "asmopt" | "assembly-optimized"
625                             | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized",
626        MethodIdentifiers => "methodIdentifiers" | "methodidentifiers" | "methods"
627                             | "method_identifiers" | "method-identifiers" | "mi",
628        GasEstimates      => "gasEstimates" | "gas" | "gas_estimates" | "gas-estimates"
629                             | "gasestimates",
630        StorageLayout     => "storageLayout" | "storage_layout" | "storage-layout"
631                             | "storagelayout" | "storage",
632        DevDoc            => "devdoc" | "dev-doc" | "devDoc",
633        Ir                => "ir" | "iR" | "IR",
634        IrOptimized       => "irOptimized" | "ir-optimized" | "iroptimized" | "iro" | "iropt",
635        Metadata          => "metadata" | "meta",
636        UserDoc           => "userdoc" | "userDoc" | "user-doc",
637        Ewasm             => "ewasm" | "e-wasm",
638        Errors            => "errors" | "er",
639        Events            => "events" | "ev",
640        StandardJson      => "standardJson" | "standard-json" | "standard_json",
641        Libraries         => "libraries" | "lib" | "libs",
642        Linearization     => "linearization" | "linearizedInheritance"
643                             | "linearized-inheritance" | "linearized_inheritance"
644                             | "linearizedBases" | "linearized-bases" | "linearized_bases",
645    }
646}
647
648impl TryFrom<ContractArtifactField> for ContractOutputSelection {
649    type Error = eyre::Error;
650
651    fn try_from(field: ContractArtifactField) -> Result<Self, Self::Error> {
652        type Caf = ContractArtifactField;
653        match field {
654            Caf::Abi => Ok(Self::Abi),
655            Caf::Bytecode => {
656                Ok(Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)))
657            }
658            Caf::DeployedBytecode => Ok(Self::Evm(EvmOutputSelection::DeployedByteCode(
659                DeployedBytecodeOutputSelection::All,
660            ))),
661            Caf::Assembly | Caf::AssemblyOptimized => Ok(Self::Evm(EvmOutputSelection::Assembly)),
662            Caf::LegacyAssembly => Ok(Self::Evm(EvmOutputSelection::LegacyAssembly)),
663            Caf::MethodIdentifiers => Ok(Self::Evm(EvmOutputSelection::MethodIdentifiers)),
664            Caf::GasEstimates => Ok(Self::Evm(EvmOutputSelection::GasEstimates)),
665            Caf::StorageLayout => Ok(Self::StorageLayout),
666            Caf::DevDoc => Ok(Self::DevDoc),
667            Caf::Ir => Ok(Self::Ir),
668            Caf::IrOptimized => Ok(Self::IrOptimized),
669            Caf::Metadata => Ok(Self::Metadata),
670            Caf::UserDoc => Ok(Self::UserDoc),
671            Caf::Ewasm => Ok(Self::Ewasm(EwasmOutputSelection::All)),
672            Caf::Errors => Ok(Self::Abi),
673            Caf::Events => Ok(Self::Abi),
674            Caf::StandardJson => {
675                Err(eyre!("StandardJson is not supported for ContractOutputSelection"))
676            }
677            Caf::Libraries => Err(eyre!("Libraries is not supported for ContractOutputSelection")),
678            Caf::Linearization => {
679                Err(eyre!("Linearization is not supported for ContractOutputSelection"))
680            }
681        }
682    }
683}
684
685impl PartialEq<ContractOutputSelection> for ContractArtifactField {
686    fn eq(&self, other: &ContractOutputSelection) -> bool {
687        type Cos = ContractOutputSelection;
688        type Eos = EvmOutputSelection;
689        matches!(
690            (self, other),
691            (Self::Abi | Self::Events | Self::Errors, Cos::Abi)
692                | (Self::Bytecode, Cos::Evm(Eos::ByteCode(_)))
693                | (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_)))
694                | (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly))
695                | (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly))
696                | (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers))
697                | (Self::GasEstimates, Cos::Evm(Eos::GasEstimates))
698                | (Self::StorageLayout, Cos::StorageLayout)
699                | (Self::DevDoc, Cos::DevDoc)
700                | (Self::Ir, Cos::Ir)
701                | (Self::IrOptimized, Cos::IrOptimized)
702                | (Self::Metadata, Cos::Metadata)
703                | (Self::UserDoc, Cos::UserDoc)
704                | (Self::Ewasm, Cos::Ewasm(_))
705        )
706    }
707}
708
709impl fmt::Display for ContractArtifactField {
710    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
711        f.write_str(self.as_str())
712    }
713}
714
715impl ContractArtifactField {
716    /// Returns true if this field does not need to be passed to the compiler.
717    pub const fn can_skip_field(&self) -> bool {
718        matches!(
719            self,
720            Self::Bytecode
721                | Self::DeployedBytecode
722                | Self::StandardJson
723                | Self::Libraries
724                | Self::Linearization
725        )
726    }
727}
728
729fn print_json(obj: &impl serde::Serialize) -> Result<()> {
730    sh_println!("{}", serde_json::to_string_pretty(obj)?)?;
731    Ok(())
732}
733
734fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> {
735    let value = serde_json::to_value(obj)?;
736    let value = key.and_then(|k| value.get(k)).unwrap_or(&value);
737    if shell::is_json() {
738        sh_println!("{}", serde_json::to_string_pretty(value)?)?;
739    } else {
740        let s = match value.as_str() {
741            Some(s) => s.to_string(),
742            None => format!("{value:#}"),
743        };
744        sh_println!("{s}")?;
745    }
746    Ok(())
747}
748
749fn print_yul(yul: Option<&str>, strip_comments: bool) -> Result<()> {
750    let Some(yul) = yul else {
751        return Err(missing_error("IR output"));
752    };
753
754    static YUL_COMMENTS: LazyLock<Regex> =
755        LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*?\*/)").unwrap());
756
757    let out = if strip_comments {
758        YUL_COMMENTS.replace_all(yul, "").into_owned()
759    } else {
760        yul.to_string()
761    };
762
763    if shell::is_json() {
764        sh_println!("{}", serde_json::to_string(&out)?)?;
765    } else {
766        sh_println!("{out}")?;
767    }
768
769    Ok(())
770}
771
772fn is_solidity_source(path: &Path) -> bool {
773    path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| ext.eq_ignore_ascii_case("sol"))
774}
775
776fn missing_error(field: &str) -> eyre::Error {
777    eyre!(
778        "{field} missing from artifact; \
779         this could be a spurious caching issue, consider running `forge clean`"
780    )
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    #[test]
788    fn contract_output_selection() {
789        for &field in ContractArtifactField::ALL {
790            if field == ContractArtifactField::StandardJson {
791                let selection: Result<ContractOutputSelection, _> = field.try_into();
792                assert!(
793                    selection
794                        .unwrap_err()
795                        .to_string()
796                        .eq("StandardJson is not supported for ContractOutputSelection")
797                );
798            } else if field == ContractArtifactField::Libraries {
799                let selection: Result<ContractOutputSelection, _> = field.try_into();
800                assert!(
801                    selection
802                        .unwrap_err()
803                        .to_string()
804                        .eq("Libraries is not supported for ContractOutputSelection")
805                );
806            } else if field == ContractArtifactField::Linearization {
807                let selection: Result<ContractOutputSelection, _> = field.try_into();
808                assert!(
809                    selection
810                        .unwrap_err()
811                        .to_string()
812                        .eq("Linearization is not supported for ContractOutputSelection")
813                );
814            } else {
815                let selection: ContractOutputSelection = field.try_into().unwrap();
816                assert_eq!(field, selection);
817
818                let s = field.as_str();
819                assert_eq!(s, field.to_string());
820                assert_eq!(s.parse::<ContractArtifactField>().unwrap(), field);
821                for alias in field.aliases() {
822                    assert_eq!(alias.parse::<ContractArtifactField>().unwrap(), field);
823                }
824            }
825        }
826    }
827}