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