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, skip_serializing_if = "HashMap::is_empty")]
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(
133        default,
134        skip_serializing_if = "IndexMap::is_empty",
135        rename = "refNodeIdToBinding"
136    )]
137    pub ref_node_id_to_binding: IndexMap<u32, BindingId>,
138
139    /// Maps a scope-creating AST node's node-ID to the scope it creates.
140    #[serde(
141        default,
142        skip_serializing_if = "HashMap::is_empty",
143        rename = "nodeIdToScope"
144    )]
145    pub node_id_to_scope: HashMap<u32, ScopeId>,
146
147    /// The program-level (module) scope. Always scopes[0].
148    pub program_scope: ScopeId,
149}
150
151impl ScopeInfo {
152    /// Look up a binding by name starting from the given scope,
153    /// walking up the parent chain. Returns None for globals.
154    pub fn get_binding(&self, scope_id: ScopeId, name: &str) -> Option<BindingId> {
155        let mut current = Some(scope_id);
156        while let Some(id) = current {
157            let scope = &self.scopes[id.0 as usize];
158            if let Some(&binding_id) = scope.bindings.get(name) {
159                return Some(binding_id);
160            }
161            current = scope.parent;
162        }
163        None
164    }
165
166    /// Look up the scope for an AST node by its unique node ID.
167    pub fn resolve_scope_by_node_id(&self, node_id: u32) -> Option<ScopeId> {
168        self.node_id_to_scope.get(&node_id).copied()
169    }
170
171    /// Resolve the scope for an AST node by node_id.
172    /// Returns None if node_id is None (the node has no scope entry) or if the
173    /// node_id doesn't map to any scope. This is expected for AST nodes that
174    /// don't create their own scope — e.g., a function body BlockStatement in
175    /// Babel shares the function's scope and never gets a _nodeId assigned by
176    /// scope extraction.
177    pub fn resolve_scope_for_node(&self, node_id: Option<u32>) -> Option<ScopeId> {
178        let nid = node_id?;
179        self.node_id_to_scope.get(&nid).copied()
180    }
181
182    /// Look up the binding for an identifier reference by its unique node ID.
183    /// Returns None for globals/unresolved references.
184    pub fn resolve_reference_by_node_id(&self, node_id: u32) -> Option<BindingId> {
185        self.ref_node_id_to_binding.get(&node_id).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_id_for_node(&self, node_id: Option<u32>) -> Option<BindingId> {
192        let nid = node_id?;
193        self.ref_node_id_to_binding.get(&nid).copied()
194    }
195
196    /// Resolve the binding for an identifier by node_id.
197    /// Returns None if node_id is None or if the identifier doesn't resolve to
198    /// a binding (i.e., it's a global/unresolved reference).
199    pub fn resolve_reference_for_node(&self, node_id: Option<u32>) -> Option<&BindingData> {
200        self.resolve_reference_id_for_node(node_id)
201            .map(|id| &self.bindings[id.0 as usize])
202    }
203
204    /// Find a binding by name within the descendants of a given scope.
205    pub fn find_binding_in_descendants(
206        &self,
207        name: &str,
208        ancestor: ScopeId,
209    ) -> Option<&BindingData> {
210        let mut descendants = std::collections::HashSet::new();
211        descendants.insert(ancestor);
212        let mut changed = true;
213        while changed {
214            changed = false;
215            for (i, scope) in self.scopes.iter().enumerate() {
216                let sid = ScopeId(i as u32);
217                if let Some(parent) = scope.parent {
218                    if descendants.contains(&parent) && !descendants.contains(&sid) {
219                        descendants.insert(sid);
220                        changed = true;
221                    }
222                }
223            }
224        }
225        for sid in &descendants {
226            let scope = &self.scopes[sid.0 as usize];
227            if let Some(id) = scope.bindings.get(name) {
228                return Some(&self.bindings[id.0 as usize]);
229            }
230        }
231        None
232    }
233
234    /// Like find_binding_in_descendants, but returns the BindingData with its id
235    /// for use in resolve_binding.
236    pub fn find_binding_id_in_descendants(
237        &self,
238        name: &str,
239        ancestor: ScopeId,
240    ) -> Option<(BindingId, &BindingData)> {
241        let mut descendants = std::collections::HashSet::new();
242        descendants.insert(ancestor);
243        let mut changed = true;
244        while changed {
245            changed = false;
246            for (i, scope) in self.scopes.iter().enumerate() {
247                let sid = ScopeId(i as u32);
248                if let Some(parent) = scope.parent {
249                    if descendants.contains(&parent) && !descendants.contains(&sid) {
250                        descendants.insert(sid);
251                        changed = true;
252                    }
253                }
254            }
255        }
256        for sid in &descendants {
257            let scope = &self.scopes[sid.0 as usize];
258            if let Some(&id) = scope.bindings.get(name) {
259                return Some((id, &self.bindings[id.0 as usize]));
260            }
261        }
262        None
263    }
264
265    /// Get all bindings declared in a scope (for hoisting iteration).
266    pub fn scope_bindings(&self, scope_id: ScopeId) -> impl Iterator<Item = &BindingData> {
267        self.scopes[scope_id.0 as usize]
268            .bindings
269            .values()
270            .map(|id| &self.bindings[id.0 as usize])
271    }
272
273    /// Get bindings from a scope AND its direct child block scopes.
274    /// In Babel, a function body's BlockStatement shares the function's scope,
275    /// so all bindings (var, const, let) appear in one scope. But our scope
276    /// extraction may split them: function scope has params/var, a child block
277    /// scope has const/let. This method merges them to match TS behavior.
278    pub fn scope_bindings_with_children(
279        &self,
280        scope_id: ScopeId,
281    ) -> impl Iterator<Item = &BindingData> {
282        let mut binding_ids: Vec<BindingId> = Vec::new();
283        // Add bindings from the scope itself
284        for &id in self.scopes[scope_id.0 as usize].bindings.values() {
285            binding_ids.push(id);
286        }
287        // Add bindings from direct child block scopes
288        for scope in self.scopes.iter() {
289            if scope.parent == Some(scope_id) && matches!(scope.kind, ScopeKind::Block) {
290                for &id in scope.bindings.values() {
291                    binding_ids.push(id);
292                }
293            }
294        }
295        binding_ids
296            .into_iter()
297            .map(|id| &self.bindings[id.0 as usize])
298    }
299
300    /// Find a block scope by matching variable names declared within it.
301    /// Used for synthetic blocks (position 0) where position-based lookup fails.
302    /// The `is_claimed` predicate allows skipping scopes already matched to other blocks.
303    pub fn find_block_scope_by_bindings(
304        &self,
305        names: &[&str],
306        ancestor: ScopeId,
307        is_claimed: impl Fn(ScopeId) -> bool,
308    ) -> Option<ScopeId> {
309        let mut descendants = std::collections::HashSet::new();
310        descendants.insert(ancestor);
311        let mut changed = true;
312        while changed {
313            changed = false;
314            for (i, scope) in self.scopes.iter().enumerate() {
315                let sid = ScopeId(i as u32);
316                if let Some(parent) = scope.parent {
317                    if descendants.contains(&parent) && !descendants.contains(&sid) {
318                        descendants.insert(sid);
319                        changed = true;
320                    }
321                }
322            }
323        }
324        for sid in &descendants {
325            let scope = &self.scopes[sid.0 as usize];
326            if matches!(scope.kind, ScopeKind::Function) {
327                continue;
328            }
329            if is_claimed(*sid) {
330                continue;
331            }
332            let all_match = names.iter().all(|name| scope.bindings.contains_key(*name));
333            if all_match {
334                return Some(*sid);
335            }
336        }
337        None
338    }
339}