Skip to main content

forge/cmd/
inspect.rs

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