Skip to main content

react_compiler_ast/
scope.rs

1use std::collections::HashMap;
2
3use indexmap::IndexMap;
4use serde::Deserialize;
5use serde::Serialize;
6
7/// Identifies a scope in the scope table. Copy-able, used as an index.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ScopeId(pub u32);
10
11/// Identifies a binding (variable declaration) in the binding table. Copy-able, used as an index.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct BindingId(pub u32);
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct ScopeData {
18    pub id: ScopeId,
19    pub parent: Option<ScopeId>,
20    pub kind: ScopeKind,
21    /// Bindings declared directly in this scope, keyed by name.
22    /// Maps to BindingId for lookup in the binding table.
23    pub bindings: HashMap<String, BindingId>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum ScopeKind {
29    Program,
30    Function,
31    Block,
32    #[serde(rename = "for")]
33    For,
34    Class,
35    Switch,
36    Catch,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct BindingData {
42    pub id: BindingId,
43    pub name: String,
44    pub kind: BindingKind,
45    /// The scope this binding is declared in.
46    pub scope: ScopeId,
47    /// The type of the declaration AST node (e.g., "FunctionDeclaration",
48    /// "VariableDeclarator"). Used by the compiler to distinguish function
49    /// declarations from variable declarations during hoisting.
50    pub declaration_type: String,
51    /// The start offset of the binding's declaration identifier.
52    /// Used to distinguish declaration sites from references in `reference_to_binding`.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub declaration_start: Option<u32>,
55    /// The node-ID of the binding's declaration identifier.
56    /// Preferred over `declaration_start` for distinguishing declarations from
57    /// references, as positions can collide for synthetic nodes at position 0.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub declaration_node_id: Option<u32>,
60    /// For import bindings: the source module and import details.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub import: Option<ImportBindingData>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "lowercase")]
67pub enum BindingKind {
68    Var,
69    Let,
70    Const,
71    Param,
72    /// Import bindings (import declarations).
73    Module,
74    /// Function declarations (hoisted).
75    Hoisted,
76    /// Other local bindings (class declarations, etc.).
77    Local,
78    /// Binding kind not recognized by the serializer.
79    Unknown,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ImportBindingData {
84    /// The module specifier string (e.g., "react" in `import {useState} from 'react'`).
85    pub source: String,
86    pub kind: ImportBindingKind,
87    /// For named imports: the imported name (e.g., "bar" in `import {bar as baz} from 'foo'`).
88    /// None for default and namespace imports.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub imported: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "lowercase")]
95pub enum ImportBindingKind {
96    Default,
97    Named,
98    Namespace,
99}
100
101/// Complete scope information for a program. Stored separately from the AST
102/// and linked via position-based lookup maps.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct ScopeInfo {
106    /// All scopes, indexed by ScopeId. scopes[id.0] gives the ScopeData for that scope.
107    pub scopes: Vec<ScopeData>,
108    /// All bindings, indexed by BindingId. bindings[id.0] gives the BindingData.
109    pub bindings: Vec<BindingData>,
110
111    /// Maps an AST node's start offset to the scope it creates.
112    ///
113    /// **NOT for identity lookups** — use `node_id_to_scope` (via `resolve_scope_for_node`)
114    /// instead. Retained only for position-range containment queries
115    /// (e.g., "is reference R inside function scope S?").
116    pub node_to_scope: HashMap<u32, ScopeId>,
117
118    /// Maps an AST node's start offset to the node's end offset.
119    /// Parallel to `node_to_scope` — used for position-range containment checks.
120    #[serde(default)]
121    pub node_to_scope_end: HashMap<u32, u32>,
122
123    /// **DEPRECATED** — retained only for Babel bridge JSON deserialization.
124    /// All backends pass empty maps; only the Babel bridge populates this.
125    /// Use `ref_node_id_to_binding` for all lookups and iteration.
126    #[serde(default)]
127    pub reference_to_binding: IndexMap<u32, BindingId>,
128
129    /// Maps an identifier reference's node-ID to the binding it resolves to.
130    /// Only present for identifiers that resolve to a binding (not globals).
131    /// Uses IndexMap to preserve insertion order.
132    #[serde(default, rename = "refNodeIdToBinding")]
133    pub ref_node_id_to_binding: IndexMap<u32, BindingId>,
134
135    /// Maps a scope-creating AST node's node-ID to the scope it creates.
136    #[serde(default, rename = "nodeIdToScope")]
137    pub node_id_to_scope: HashMap<u32, ScopeId>,
138
139    /// The program-level (module) scope. Always scopes[0].
140    pub program_scope: ScopeId,
141}
142
143impl ScopeInfo {
144    /// Look up a binding by name starting from the given scope,
145    /// walking up the parent chain. Returns None for globals.
146    pub fn get_binding(&self, scope_id: ScopeId, name: &str) -> Option<BindingId> {
147        let mut current = Some(scope_id);
148        while let Some(id) = current {
149            let scope = &self.scopes[id.0 as usize];
150            if let Some(&binding_id) = scope.bindings.get(name) {
151                return Some(binding_id);
152            }
153            current = scope.parent;
154        }
155        None
156    }
157
158    /// Look up the scope for an AST node by its unique node ID.
159    pub fn resolve_scope_by_node_id(&self, node_id: u32) -> Option<ScopeId> {
160        self.node_id_to_scope.get(&node_id).copied()
161    }
162
163    /// Resolve the scope for an AST node by node_id.
164    /// Returns None if node_id is None (the node has no scope entry) or if the
165    /// node_id doesn't map to any scope. This is expected for AST nodes that
166    /// don't create their own scope — e.g., a function body BlockStatement in
167    /// Babel shares the function's scope and never gets a _nodeId assigned by
168    /// scope extraction.
169    pub fn resolve_scope_for_node(&self, node_id: Option<u32>) -> Option<ScopeId> {
170        let nid = node_id?;
171        self.node_id_to_scope.get(&nid).copied()
172    }
173
174    /// Look up the binding for an identifier reference by its unique node ID.
175    /// Returns None for globals/unresolved references.
176    pub fn resolve_reference_by_node_id(&self, node_id: u32) -> Option<BindingId> {
177        self.ref_node_id_to_binding.get(&node_id).copied()
178    }
179
180    /// Resolve the binding for an identifier by node_id.
181    /// Returns None if node_id is None or if the identifier doesn't resolve to
182    /// a binding (i.e., it's a global/unresolved reference).
183    pub fn resolve_reference_id_for_node(&self, node_id: Option<u32>) -> Option<BindingId> {
184        let nid = node_id?;
185        self.ref_node_id_to_binding.get(&nid).copied()
186    }
187
188    /// Resolve the binding for an identifier by node_id.
189    /// Returns None if node_id is None or if the identifier doesn't resolve to
190    /// a binding (i.e., it's a global/unresolved reference).
191    pub fn resolve_reference_for_node(&self, node_id: Option<u32>) -> Option<&BindingData> {
192        self.resolve_reference_id_for_node(node_id)
193            .map(|id| &self.bindings[id.0 as usize])
194    }
195
196    /// Find a binding by name within the descendants of a given scope.
197    pub fn find_binding_in_descendants(
198        &self,
199        name: &str,
200        ancestor: ScopeId,
201    ) -> Option<&BindingData> {
202        let mut descendants = std::collections::HashSet::new();
203        descendants.insert(ancestor);
204        let mut changed = true;
205        while changed {
206            changed = false;
207            for (i, scope) in self.scopes.iter().enumerate() {
208                let sid = ScopeId(i as u32);
209                if let Some(parent) = scope.parent {
210                    if descendants.contains(&parent) && !descendants.contains(&sid) {
211                        descendants.insert(sid);
212                        changed = true;
213                    }
214                }
215            }
216        }
217        for sid in &descendants {
218            let scope = &self.scopes[sid.0 as usize];
219            if let Some(id) = scope.bindings.get(name) {
220                return Some(&self.bindings[id.0 as usize]);
221            }
222        }
223        None
224    }
225
226    /// Like find_binding_in_descendants, but returns the BindingData with its id
227    /// for use in resolve_binding.
228    pub fn find_binding_id_in_descendants(
229        &self,
230        name: &str,
231        ancestor: ScopeId,
232    ) -> Option<(BindingId, &BindingData)> {
233        let mut descendants = std::collections::HashSet::new();
234        descendants.insert(ancestor);
235        let mut changed = true;
236        while changed {
237            changed = false;
238            for (i, scope) in self.scopes.iter().enumerate() {
239                let sid = ScopeId(i as u32);
240                if let Some(parent) = scope.parent {
241                    if descendants.contains(&parent) && !descendants.contains(&sid) {
242                        descendants.insert(sid);
243                        changed = true;
244                    }
245                }
246            }
247        }
248        for sid in &descendants {
249            let scope = &self.scopes[sid.0 as usize];
250            if let Some(&id) = scope.bindings.get(name) {
251                return Some((id, &self.bindings[id.0 as usize]));
252            }
253        }
254        None
255    }
256
257    /// Get all bindings declared in a scope (for hoisting iteration).
258    pub fn scope_bindings(&self, scope_id: ScopeId) -> impl Iterator<Item = &BindingData> {
259        self.scopes[scope_id.0 as usize]
260            .bindings
261            .values()
262            .map(|id| &self.bindings[id.0 as usize])
263    }
264
265    /// Get bindings from a scope AND its direct child block scopes.
266    /// In Babel, a function body's BlockStatement shares the function's scope,
267    /// so all bindings (var, const, let) appear in one scope. But our scope
268    /// extraction may split them: function scope has params/var, a child block
269    /// scope has const/let. This method merges them to match TS behavior.
270    pub fn scope_bindings_with_children(
271        &self,
272        scope_id: ScopeId,
273    ) -> impl Iterator<Item = &BindingData> {
274        let mut binding_ids: Vec<BindingId> = Vec::new();
275        // Add bindings from the scope itself
276        for &id in self.scopes[scope_id.0 as usize].bindings.values() {
277            binding_ids.push(id);
278        }
279        // Add bindings from direct child block scopes
280        for scope in self.scopes.iter() {
281            if scope.parent == Some(scope_id) && matches!(scope.kind, ScopeKind::Block) {
282                for &id in scope.bindings.values() {
283                    binding_ids.push(id);
284                }
285            }
286        }
287        binding_ids
288            .into_iter()
289            .map(|id| &self.bindings[id.0 as usize])
290    }
291
292    /// Find a block scope by matching variable names declared within it.
293    /// Used for synthetic blocks (position 0) where position-based lookup fails.
294    /// The `is_claimed` predicate allows skipping scopes already matched to other blocks.
295    pub fn find_block_scope_by_bindings(
296        &self,
297        names: &[&str],
298        ancestor: ScopeId,
299        is_claimed: impl Fn(ScopeId) -> bool,
300    ) -> Option<ScopeId> {
301        let mut descendants = std::collections::HashSet::new();
302        descendants.insert(ancestor);
303        let mut changed = true;
304        while changed {
305            changed = false;
306            for (i, scope) in self.scopes.iter().enumerate() {
307                let sid = ScopeId(i as u32);
308                if let Some(parent) = scope.parent {
309                    if descendants.contains(&parent) && !descendants.contains(&sid) {
310                        descendants.insert(sid);
311                        changed = true;
312                    }
313                }
314            }
315        }
316        for sid in &descendants {
317            let scope = &self.scopes[sid.0 as usize];
318            if matches!(scope.kind, ScopeKind::Function) {
319                continue;
320            }
321            if is_claimed(*sid) {
322                continue;
323            }
324            let all_match = names.iter().all(|name| scope.bindings.contains_key(*name));
325            if all_match {
326                return Some(*sid);
327            }
328        }
329        None
330    }
331}