Skip to main content

react_compiler_ast/
scope.rs

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