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