Skip to main content

forge/cmd/
inspect.rs

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