Skip to main content

forge/cmd/
inspect.rs

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