Skip to main content

forge/cmd/
inspect.rs

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