main
rs 339 lines 12.7 KB
Raw
1 use rustc_hash::{FxBuildHasher, FxHashMap};
2
3 use indexmap::IndexMap;
4 use serde::Deserialize;
5 use 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)]
9 pub 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)]
13 pub struct BindingId(pub u32);
14
15 #[derive(Debug, Clone, Serialize, Deserialize)]
16 #[serde(rename_all = "camelCase")]
17 pub 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: FxHashMap<String, BindingId>,
24 }
25
26 #[derive(Debug, Clone, Serialize, Deserialize)]
27 #[serde(rename_all = "lowercase")]
28 pub 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")]
41 pub 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")]
67 pub 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)]
83 pub 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")]
95 pub 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")]
105 pub 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: FxHashMap<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 = "FxHashMap::is_empty")]
121 pub node_to_scope_end: FxHashMap<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, FxBuildHasher>,
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, FxBuildHasher>,
138
139 /// Maps a scope-creating AST node's node-ID to the scope it creates.
140 #[serde(
141 default,
142 skip_serializing_if = "FxHashMap::is_empty",
143 rename = "nodeIdToScope"
144 )]
145 pub node_id_to_scope: FxHashMap<u32, ScopeId>,
146
147 /// The program-level (module) scope. Always scopes[0].
148 pub program_scope: ScopeId,
149 }
150
151 impl 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 = rustc_hash::FxHashSet::default();
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 = rustc_hash::FxHashSet::default();
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 = rustc_hash::FxHashSet::default();
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 }