| 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 | } |