foundry_cheatcodes/inspector/
analysis.rs

1//! Cheatcode information, extracted from the syntactic and semantic analysis of the sources.
2
3use foundry_common::fmt::{StructDefinitions, TypeDefMap};
4use solar::sema::{self, Compiler, Gcx, hir};
5use std::sync::{Arc, OnceLock};
6use thiserror::Error;
7
8/// Represents a failure in one of the lazy analysis steps.
9#[derive(Debug, Clone, PartialEq, Eq, Error)]
10pub enum AnalysisError {
11    /// Indicates that the resolution of struct definitions failed.
12    #[error("unable to resolve struct definitions")]
13    StructDefinitionsResolutionFailed,
14}
15
16/// Provides cached, on-demand syntactic and semantic analysis of a completed `Compiler` instance.
17///
18/// This struct acts as a facade over the `Compiler`, offering lazy-loaded analysis for tools like
19/// cheatcode inspectors. It assumes the compiler has already completed parsing and lowering.
20///
21/// # Adding with new analyses types
22///
23/// To add support for a new type of cached analysis, follow this pattern:
24///
25/// 1. Add a new `pub OnceCell<Result<T, AnalysisError>>` field to `CheatcodeAnalysis`, where `T` is
26///    the type of the data that you are adding support for.
27///
28/// 2. Implement a getter method for the new field. Inside the getter, use
29///    `self.field.get_or_init()` to compute and cache the value on the first call.
30///
31/// 3. Inside the closure passed to `get_or_init()`, create a dedicated visitor to traverse the HIR
32///    using `self.compiler.enter()` and collect the required data.
33///
34/// This ensures all analyses remain lazy, efficient, and consistent with the existing design.
35#[derive(Clone)]
36pub struct CheatcodeAnalysis {
37    /// A shared, thread-safe reference to solar's `Compiler` instance.
38    pub compiler: Arc<Compiler>,
39
40    /// Cached struct definitions in the sources.
41    /// Used to keep field order when parsing JSON values.
42    struct_defs: OnceLock<Result<StructDefinitions, AnalysisError>>,
43}
44
45impl std::fmt::Debug for CheatcodeAnalysis {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("CheatcodeAnalysis")
48            .field("compiler", &"<compiler>")
49            .field("struct_defs", &self.struct_defs)
50            .finish()
51    }
52}
53
54impl CheatcodeAnalysis {
55    pub fn new(compiler: Arc<solar::sema::Compiler>) -> Self {
56        Self { compiler, struct_defs: OnceLock::new() }
57    }
58
59    /// Lazily initializes and returns the struct definitions.
60    pub fn struct_defs(&self) -> Result<&StructDefinitions, &AnalysisError> {
61        self.struct_defs
62            .get_or_init(|| {
63                self.compiler.enter(|compiler| {
64                    let gcx = compiler.gcx();
65
66                    StructDefinitionResolver::new(gcx).process()
67                })
68            })
69            .as_ref()
70    }
71}
72
73// -- STRUCT DEFINITIONS -------------------------------------------------------
74
75/// Generates a map of all struct definitions from the HIR using the resolved `Ty` system.
76struct StructDefinitionResolver<'gcx> {
77    gcx: Gcx<'gcx>,
78    struct_defs: TypeDefMap,
79}
80
81impl<'gcx> StructDefinitionResolver<'gcx> {
82    /// Constructs a new generator.
83    pub fn new(gcx: Gcx<'gcx>) -> Self {
84        Self { gcx, struct_defs: TypeDefMap::new() }
85    }
86
87    /// Processes the HIR to generate all the struct definitions.
88    pub fn process(mut self) -> Result<StructDefinitions, AnalysisError> {
89        for id in self.hir().strukt_ids() {
90            self.resolve_struct_definition(id)?;
91        }
92        Ok(self.struct_defs.into())
93    }
94
95    #[inline]
96    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
97        &self.gcx.hir
98    }
99
100    /// The recursive core of the generator. Resolves a single struct and adds it to the cache.
101    fn resolve_struct_definition(&mut self, id: hir::StructId) -> Result<(), AnalysisError> {
102        let qualified_name = self.get_fully_qualified_name(id);
103        if self.struct_defs.contains_key(&qualified_name) {
104            return Ok(());
105        }
106
107        let hir = self.hir();
108        let strukt = hir.strukt(id);
109        let mut fields = Vec::with_capacity(strukt.fields.len());
110
111        for &field_id in strukt.fields {
112            let var = hir.variable(field_id);
113            let name =
114                var.name.ok_or(AnalysisError::StructDefinitionsResolutionFailed)?.to_string();
115            if let Some(ty_str) = self.ty_to_string(self.gcx.type_of_hir_ty(&var.ty)) {
116                fields.push((name, ty_str));
117            }
118        }
119
120        // Only insert if there are fields, to avoid adding empty entries
121        if !fields.is_empty() {
122            self.struct_defs.insert(qualified_name, fields);
123        }
124
125        Ok(())
126    }
127
128    /// Converts a resolved `Ty` into its canonical string representation.
129    fn ty_to_string(&mut self, ty: sema::Ty<'gcx>) -> Option<String> {
130        let ty = ty.peel_refs();
131        let res = match ty.kind {
132            sema::ty::TyKind::Elementary(e) => e.to_string(),
133            sema::ty::TyKind::Array(ty, size) => {
134                let inner_type = self.ty_to_string(ty)?;
135                format!("{inner_type}[{size}]")
136            }
137            sema::ty::TyKind::DynArray(ty) => {
138                let inner_type = self.ty_to_string(ty)?;
139                format!("{inner_type}[]")
140            }
141            sema::ty::TyKind::Struct(id) => {
142                // Ensure the nested struct is resolved before proceeding.
143                self.resolve_struct_definition(id).ok()?;
144                self.get_fully_qualified_name(id)
145            }
146            sema::ty::TyKind::Udvt(ty, _) => self.ty_to_string(ty)?,
147            // For now, map enums to `uint8`
148            sema::ty::TyKind::Enum(_) => "uint8".to_string(),
149            // For now, map contracts to `address`
150            sema::ty::TyKind::Contract(_) => "address".to_string(),
151            // Explicitly disallow unsupported types
152            _ => return None,
153        };
154
155        Some(res)
156    }
157
158    /// Helper to get the fully qualified name `Contract.Struct`.
159    fn get_fully_qualified_name(&self, id: hir::StructId) -> String {
160        let hir = self.hir();
161        let strukt = hir.strukt(id);
162        if let Some(contract_id) = strukt.contract {
163            format!("{}.{}", hir.contract(contract_id).name.as_str(), strukt.name.as_str())
164        } else {
165            strukt.name.as_str().into()
166        }
167    }
168}