Skip to main content

react_compiler_swc/
convert_scope.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5
6use std::collections::HashMap;
7use std::collections::HashSet;
8
9use indexmap::IndexMap;
10use react_compiler_ast::scope::*;
11use swc_ecma_ast::*;
12use swc_ecma_visit::Visit;
13use swc_ecma_visit::VisitWith;
14
15/// Helper to convert an SWC `Str` node's value to a Rust String.
16/// `Str.value` is a `Wtf8Atom` which doesn't implement `Display`,
17/// so we go through `Atom` via lossy conversion.
18fn str_value_to_string(s: &Str) -> String {
19    s.value.to_atom_lossy().to_string()
20}
21
22/// Build scope information from an SWC Module AST.
23///
24/// This performs two passes over the AST:
25/// 1. Build the scope tree and collect all bindings
26/// 2. Resolve identifier references to their bindings
27pub fn build_scope_info(module: &Module) -> ScopeInfo {
28    // Pass 1: Build scope tree and collect bindings
29    let mut collector = ScopeCollector::new();
30    collector.visit_module(module);
31
32    // Pass 2: Resolve references
33    // We scope the resolver borrow so we can move out of collector afterwards.
34    // Build ref_node_id_to_binding directly. In SWC, node_id == span.lo
35    // (unique positions), so the resolver's position-keyed map serves as
36    // both reference_to_binding and ref_node_id_to_binding.
37    let ref_node_id_to_binding = {
38        let mut resolver = ReferenceResolver::new(&collector);
39        resolver.visit_module(module);
40
41        // Also map declaration identifiers to their bindings
42        for binding in &collector.bindings {
43            if let Some(start) = binding.declaration_start {
44                resolver
45                    .reference_to_binding
46                    .entry(start)
47                    .or_insert(binding.id);
48            }
49        }
50
51        // Function redeclarations: resolve the second `function x()`'s ident
52        // to the first declaration's binding (overwrites any earlier entry).
53        for (start, binding_id) in &collector.redeclaration_refs {
54            resolver.reference_to_binding.insert(*start, *binding_id);
55        }
56
57        resolver.reference_to_binding
58    };
59
60    let node_id_to_scope = collector.node_to_scope.clone();
61
62    ScopeInfo {
63        scopes: collector.scopes,
64        bindings: collector.bindings,
65        node_to_scope: collector.node_to_scope,
66        node_to_scope_end: collector.node_to_scope_end,
67        reference_to_binding: indexmap::IndexMap::new(),
68        ref_node_id_to_binding,
69        node_id_to_scope,
70        program_scope: ScopeId(0),
71    }
72}
73
74// ── Pass 1: Scope tree + binding collection ─────────────────────────────────
75
76struct ScopeCollector {
77    scopes: Vec<ScopeData>,
78    bindings: Vec<BindingData>,
79    node_to_scope: HashMap<u32, ScopeId>,
80    node_to_scope_end: HashMap<u32, u32>,
81    /// Stack of scope IDs representing the current nesting.
82    scope_stack: Vec<ScopeId>,
83    /// Set of span starts for block statements that are direct function/catch bodies.
84    /// These should NOT create a separate Block scope.
85    function_body_spans: HashSet<u32>,
86    /// Function declarations that redeclare an existing hoisted name resolve to
87    /// the first binding's `BindingId` (like babel's `constantViolations`), so
88    /// the compiler treats the redeclaration as a reassignment rather than a
89    /// new binding.
90    redeclaration_refs: HashMap<u32, BindingId>,
91}
92
93impl ScopeCollector {
94    fn new() -> Self {
95        Self {
96            scopes: Vec::new(),
97            bindings: Vec::new(),
98            node_to_scope: HashMap::new(),
99            node_to_scope_end: HashMap::new(),
100            scope_stack: Vec::new(),
101            function_body_spans: HashSet::new(),
102            redeclaration_refs: HashMap::new(),
103        }
104    }
105
106    fn current_scope(&self) -> ScopeId {
107        *self.scope_stack.last().expect("scope stack is empty")
108    }
109
110    fn push_scope(&mut self, kind: ScopeKind, node_start: u32, node_end: u32) -> ScopeId {
111        let id = ScopeId(self.scopes.len() as u32);
112        let parent = self.scope_stack.last().copied();
113        self.scopes.push(ScopeData {
114            id,
115            parent,
116            kind,
117            bindings: HashMap::new(),
118        });
119        self.node_to_scope.insert(node_start, id);
120        if node_end > node_start {
121            self.node_to_scope_end.insert(node_start, node_end);
122        }
123        self.scope_stack.push(id);
124        id
125    }
126
127    fn pop_scope(&mut self) {
128        self.scope_stack.pop();
129    }
130
131    /// Find the nearest enclosing function or program scope (for hoisting `var` and function decls).
132    fn enclosing_function_scope(&self) -> ScopeId {
133        for &scope_id in self.scope_stack.iter().rev() {
134            let scope = &self.scopes[scope_id.0 as usize];
135            match scope.kind {
136                ScopeKind::Function | ScopeKind::Program => return scope_id,
137                _ => {}
138            }
139        }
140        ScopeId(0)
141    }
142
143    fn add_binding(
144        &mut self,
145        name: String,
146        kind: BindingKind,
147        scope: ScopeId,
148        declaration_type: String,
149        declaration_start: Option<u32>,
150        import: Option<ImportBindingData>,
151    ) -> BindingId {
152        let id = BindingId(self.bindings.len() as u32);
153        self.bindings.push(BindingData {
154            id,
155            name: name.clone(),
156            kind,
157            scope,
158            declaration_type,
159            declaration_start,
160            declaration_node_id: declaration_start,
161            import,
162        });
163        self.scopes[scope.0 as usize].bindings.insert(name, id);
164        id
165    }
166
167    /// Extract all binding identifiers from a pattern, adding each as a binding.
168    fn collect_pat_bindings(
169        &mut self,
170        pat: &Pat,
171        kind: BindingKind,
172        scope: ScopeId,
173        declaration_type: &str,
174    ) {
175        match pat {
176            Pat::Ident(binding_ident) => {
177                let name = binding_ident.id.sym.to_string();
178                let start = binding_ident.id.span.lo.0;
179                self.add_binding(
180                    name,
181                    kind,
182                    scope,
183                    declaration_type.to_string(),
184                    Some(start),
185                    None,
186                );
187            }
188            Pat::Array(arr) => {
189                for elem in &arr.elems {
190                    if let Some(p) = elem {
191                        self.collect_pat_bindings(p, kind.clone(), scope, declaration_type);
192                    }
193                }
194            }
195            Pat::Object(obj) => {
196                for prop in &obj.props {
197                    match prop {
198                        ObjectPatProp::KeyValue(kv) => {
199                            self.collect_pat_bindings(
200                                &kv.value,
201                                kind.clone(),
202                                scope,
203                                declaration_type,
204                            );
205                        }
206                        ObjectPatProp::Assign(assign) => {
207                            let name = assign.key.sym.to_string();
208                            let start = assign.key.span.lo.0;
209                            self.add_binding(
210                                name,
211                                kind.clone(),
212                                scope,
213                                declaration_type.to_string(),
214                                Some(start),
215                                None,
216                            );
217                        }
218                        ObjectPatProp::Rest(rest) => {
219                            self.collect_pat_bindings(
220                                &rest.arg,
221                                kind.clone(),
222                                scope,
223                                declaration_type,
224                            );
225                        }
226                    }
227                }
228            }
229            Pat::Rest(rest) => {
230                self.collect_pat_bindings(&rest.arg, kind, scope, declaration_type);
231            }
232            Pat::Assign(assign) => {
233                self.collect_pat_bindings(&assign.left, kind, scope, declaration_type);
234            }
235            Pat::Expr(_) | Pat::Invalid(_) => {}
236        }
237    }
238
239    /// Visit a function's internals (params + body), creating the function scope.
240    /// Used for method definitions and other Function nodes not covered by FnDecl/FnExpr.
241    fn visit_function_inner(&mut self, function: &Function) {
242        let func_start = function.span.lo.0;
243        self.push_scope(ScopeKind::Function, func_start, function.span.hi.0);
244
245        for param in &function.params {
246            self.collect_pat_bindings(
247                &param.pat,
248                BindingKind::Param,
249                self.current_scope(),
250                "FormalParameter",
251            );
252        }
253
254        if let Some(body) = &function.body {
255            self.function_body_spans.insert(body.span.lo.0);
256            body.visit_with(self);
257        }
258
259        self.pop_scope();
260    }
261}
262
263impl Visit for ScopeCollector {
264    fn visit_module(&mut self, module: &Module) {
265        self.push_scope(ScopeKind::Program, module.span.lo.0, module.span.hi.0);
266        module.visit_children_with(self);
267        self.pop_scope();
268    }
269
270    fn visit_import_decl(&mut self, import: &ImportDecl) {
271        let source = str_value_to_string(&import.src);
272        let program_scope = ScopeId(0);
273
274        for spec in &import.specifiers {
275            match spec {
276                ImportSpecifier::Named(named) => {
277                    let local_name = named.local.sym.to_string();
278                    let start = named.local.span.lo.0;
279                    let imported_name = match &named.imported {
280                        Some(ModuleExportName::Ident(ident)) => Some(ident.sym.to_string()),
281                        Some(ModuleExportName::Str(s)) => Some(str_value_to_string(s)),
282                        None => Some(local_name.clone()),
283                    };
284                    self.add_binding(
285                        local_name,
286                        BindingKind::Module,
287                        program_scope,
288                        "ImportSpecifier".to_string(),
289                        Some(start),
290                        Some(ImportBindingData {
291                            source: source.clone(),
292                            kind: ImportBindingKind::Named,
293                            imported: imported_name,
294                        }),
295                    );
296                }
297                ImportSpecifier::Default(default) => {
298                    let local_name = default.local.sym.to_string();
299                    let start = default.local.span.lo.0;
300                    self.add_binding(
301                        local_name,
302                        BindingKind::Module,
303                        program_scope,
304                        "ImportDefaultSpecifier".to_string(),
305                        Some(start),
306                        Some(ImportBindingData {
307                            source: source.clone(),
308                            kind: ImportBindingKind::Default,
309                            imported: None,
310                        }),
311                    );
312                }
313                ImportSpecifier::Namespace(ns) => {
314                    let local_name = ns.local.sym.to_string();
315                    let start = ns.local.span.lo.0;
316                    self.add_binding(
317                        local_name,
318                        BindingKind::Module,
319                        program_scope,
320                        "ImportNamespaceSpecifier".to_string(),
321                        Some(start),
322                        Some(ImportBindingData {
323                            source: source.clone(),
324                            kind: ImportBindingKind::Namespace,
325                            imported: None,
326                        }),
327                    );
328                }
329            }
330        }
331    }
332
333    fn visit_var_decl(&mut self, var_decl: &VarDecl) {
334        let (kind, declaration_type) = match var_decl.kind {
335            VarDeclKind::Var => (BindingKind::Var, "VariableDeclarator"),
336            VarDeclKind::Let => (BindingKind::Let, "VariableDeclarator"),
337            VarDeclKind::Const => (BindingKind::Const, "VariableDeclarator"),
338        };
339
340        let target_scope = match var_decl.kind {
341            VarDeclKind::Var => self.enclosing_function_scope(),
342            VarDeclKind::Let | VarDeclKind::Const => self.current_scope(),
343        };
344
345        for declarator in &var_decl.decls {
346            self.collect_pat_bindings(
347                &declarator.name,
348                kind.clone(),
349                target_scope,
350                declaration_type,
351            );
352            // Visit initializers so nested functions/arrows get their scopes
353            if let Some(init) = &declarator.init {
354                init.visit_with(self);
355            }
356        }
357    }
358
359    fn visit_fn_decl(&mut self, fn_decl: &FnDecl) {
360        // Function declarations are hoisted to the enclosing function/program scope
361        let hoist_scope = self.enclosing_function_scope();
362        let name = fn_decl.ident.sym.to_string();
363        let start = fn_decl.ident.span.lo.0;
364        if let Some(&existing_id) =
365            self.scopes[hoist_scope.0 as usize].bindings.get(&name)
366        {
367            self.redeclaration_refs.insert(start, existing_id);
368        } else {
369            self.add_binding(
370                name,
371                BindingKind::Hoisted,
372                hoist_scope,
373                "FunctionDeclaration".to_string(),
374                Some(start),
375                None,
376            );
377        }
378
379        self.visit_function_inner(&fn_decl.function);
380    }
381
382    fn visit_export_default_decl(&mut self, decl: &ExportDefaultDecl) {
383        // For `export default function foo(...)`, the function name should be
384        // hoisted to the enclosing scope (like FnDecl), not bound only in the
385        // function's own scope (like FnExpr).
386        match &decl.decl {
387            DefaultDecl::Fn(fn_expr) => {
388                if let Some(ident) = &fn_expr.ident {
389                    let hoist_scope = self.enclosing_function_scope();
390                    let name = ident.sym.to_string();
391                    let start = ident.span.lo.0;
392                    self.add_binding(
393                        name,
394                        BindingKind::Hoisted,
395                        hoist_scope,
396                        "FunctionDeclaration".to_string(),
397                        Some(start),
398                        None,
399                    );
400                }
401                self.visit_function_inner(&fn_expr.function);
402            }
403            DefaultDecl::Class(class_expr) => {
404                if let Some(ident) = &class_expr.ident {
405                    let name = ident.sym.to_string();
406                    let start = ident.span.lo.0;
407                    self.add_binding(
408                        name,
409                        BindingKind::Local,
410                        self.current_scope(),
411                        "ClassDeclaration".to_string(),
412                        Some(start),
413                        None,
414                    );
415                }
416                self.push_scope(
417                    ScopeKind::Class,
418                    class_expr.class.span.lo.0,
419                    class_expr.class.span.hi.0,
420                );
421                class_expr.class.visit_children_with(self);
422                self.pop_scope();
423            }
424            DefaultDecl::TsInterfaceDecl(d) => {
425                d.visit_with(self);
426            }
427        }
428    }
429
430    fn visit_fn_expr(&mut self, fn_expr: &FnExpr) {
431        let func_start = fn_expr.function.span.lo.0;
432        self.push_scope(ScopeKind::Function, func_start, fn_expr.function.span.hi.0);
433
434        // Named function expressions bind their name in the function scope
435        if let Some(ident) = &fn_expr.ident {
436            let name = ident.sym.to_string();
437            let start = ident.span.lo.0;
438            self.add_binding(
439                name,
440                BindingKind::Local,
441                self.current_scope(),
442                "FunctionExpression".to_string(),
443                Some(start),
444                None,
445            );
446        }
447
448        for param in &fn_expr.function.params {
449            self.collect_pat_bindings(
450                &param.pat,
451                BindingKind::Param,
452                self.current_scope(),
453                "FormalParameter",
454            );
455        }
456
457        if let Some(body) = &fn_expr.function.body {
458            self.function_body_spans.insert(body.span.lo.0);
459            body.visit_with(self);
460        }
461
462        self.pop_scope();
463    }
464
465    fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) {
466        let func_start = arrow.span.lo.0;
467        self.push_scope(ScopeKind::Function, func_start, arrow.span.hi.0);
468
469        for param in &arrow.params {
470            self.collect_pat_bindings(
471                param,
472                BindingKind::Param,
473                self.current_scope(),
474                "FormalParameter",
475            );
476        }
477
478        match &*arrow.body {
479            BlockStmtOrExpr::BlockStmt(block) => {
480                self.function_body_spans.insert(block.span.lo.0);
481                block.visit_with(self);
482            }
483            BlockStmtOrExpr::Expr(expr) => {
484                expr.visit_with(self);
485            }
486        }
487
488        self.pop_scope();
489    }
490
491    fn visit_block_stmt(&mut self, block: &BlockStmt) {
492        if self.function_body_spans.remove(&block.span.lo.0) {
493            // This block is a function/catch body — don't create a separate scope
494            block.visit_children_with(self);
495        } else {
496            self.push_scope(ScopeKind::Block, block.span.lo.0, block.span.hi.0);
497            block.visit_children_with(self);
498            self.pop_scope();
499        }
500    }
501
502    fn visit_for_stmt(&mut self, for_stmt: &ForStmt) {
503        self.push_scope(ScopeKind::For, for_stmt.span.lo.0, for_stmt.span.hi.0);
504
505        if let Some(init) = &for_stmt.init {
506            init.visit_with(self);
507        }
508        if let Some(test) = &for_stmt.test {
509            test.visit_with(self);
510        }
511        if let Some(update) = &for_stmt.update {
512            update.visit_with(self);
513        }
514        for_stmt.body.visit_with(self);
515
516        self.pop_scope();
517    }
518
519    fn visit_for_in_stmt(&mut self, for_in: &ForInStmt) {
520        self.push_scope(ScopeKind::For, for_in.span.lo.0, for_in.span.hi.0);
521        for_in.left.visit_with(self);
522        for_in.right.visit_with(self);
523        for_in.body.visit_with(self);
524        self.pop_scope();
525    }
526
527    fn visit_for_of_stmt(&mut self, for_of: &ForOfStmt) {
528        self.push_scope(ScopeKind::For, for_of.span.lo.0, for_of.span.hi.0);
529        for_of.left.visit_with(self);
530        for_of.right.visit_with(self);
531        for_of.body.visit_with(self);
532        self.pop_scope();
533    }
534
535    fn visit_catch_clause(&mut self, catch: &CatchClause) {
536        self.push_scope(ScopeKind::Catch, catch.span.lo.0, catch.span.hi.0);
537
538        if let Some(param) = &catch.param {
539            self.collect_pat_bindings(param, BindingKind::Let, self.current_scope(), "CatchClause");
540        }
541
542        // Mark catch body as already scoped (the catch scope covers it)
543        self.function_body_spans.insert(catch.body.span.lo.0);
544        catch.body.visit_with(self);
545
546        self.pop_scope();
547    }
548
549    fn visit_switch_stmt(&mut self, switch: &SwitchStmt) {
550        // Visit the discriminant in the outer scope
551        switch.discriminant.visit_with(self);
552
553        self.push_scope(ScopeKind::Switch, switch.span.lo.0, switch.span.hi.0);
554        for case in &switch.cases {
555            case.visit_with(self);
556        }
557        self.pop_scope();
558    }
559
560    fn visit_class_decl(&mut self, class_decl: &ClassDecl) {
561        let name = class_decl.ident.sym.to_string();
562        let start = class_decl.ident.span.lo.0;
563        self.add_binding(
564            name,
565            BindingKind::Local,
566            self.current_scope(),
567            "ClassDeclaration".to_string(),
568            Some(start),
569            None,
570        );
571
572        self.push_scope(
573            ScopeKind::Class,
574            class_decl.class.span.lo.0,
575            class_decl.class.span.hi.0,
576        );
577        class_decl.class.visit_children_with(self);
578        self.pop_scope();
579    }
580
581    fn visit_class_expr(&mut self, class_expr: &ClassExpr) {
582        self.push_scope(
583            ScopeKind::Class,
584            class_expr.class.span.lo.0,
585            class_expr.class.span.hi.0,
586        );
587
588        if let Some(ident) = &class_expr.ident {
589            let name = ident.sym.to_string();
590            let start = ident.span.lo.0;
591            self.add_binding(
592                name,
593                BindingKind::Local,
594                self.current_scope(),
595                "ClassExpression".to_string(),
596                Some(start),
597                None,
598            );
599        }
600
601        class_expr.class.visit_children_with(self);
602        self.pop_scope();
603    }
604
605    // Method definitions contain a Function node. We intercept here
606    // so that the Function gets its own scope with params.
607    fn visit_function(&mut self, f: &Function) {
608        // This is reached for object/class methods via default traversal.
609        self.visit_function_inner(f);
610    }
611}
612
613// ── Pass 2: Reference resolution ────────────────────────────────────────────
614
615struct ReferenceResolver<'a> {
616    scopes: &'a [ScopeData],
617    #[allow(dead_code)]
618    bindings: &'a [BindingData],
619    node_to_scope: &'a HashMap<u32, ScopeId>,
620    reference_to_binding: IndexMap<u32, BindingId>,
621    /// Stack of scope IDs for resolution
622    scope_stack: Vec<ScopeId>,
623    /// Declaration positions to skip (these are binding sites, not references)
624    declaration_starts: HashSet<u32>,
625    /// Span starts for block statements that are direct function/catch bodies.
626    function_body_spans: HashSet<u32>,
627}
628
629impl<'a> ReferenceResolver<'a> {
630    fn new(collector: &'a ScopeCollector) -> Self {
631        let mut declaration_starts = HashSet::new();
632        for binding in &collector.bindings {
633            if let Some(start) = binding.declaration_start {
634                declaration_starts.insert(start);
635            }
636        }
637        Self {
638            scopes: &collector.scopes,
639            bindings: &collector.bindings,
640            node_to_scope: &collector.node_to_scope,
641            reference_to_binding: IndexMap::new(),
642            scope_stack: Vec::new(),
643            declaration_starts,
644            function_body_spans: HashSet::new(),
645        }
646    }
647
648    fn current_scope(&self) -> ScopeId {
649        *self.scope_stack.last().expect("scope stack is empty")
650    }
651
652    fn resolve_ident(&mut self, name: &str, start: u32) {
653        // Skip declaration sites — they'll be added separately
654        if self.declaration_starts.contains(&start) {
655            return;
656        }
657
658        // Walk up the scope chain to find the binding
659        let mut current = Some(self.current_scope());
660        while let Some(scope_id) = current {
661            let scope = &self.scopes[scope_id.0 as usize];
662            if let Some(&binding_id) = scope.bindings.get(name) {
663                self.reference_to_binding.insert(start, binding_id);
664                return;
665            }
666            current = scope.parent;
667        }
668        // Not found — it's a global, don't record it
669    }
670
671    fn find_scope_at(&self, node_start: u32) -> Option<&ScopeId> {
672        self.node_to_scope.get(&node_start)
673    }
674
675    /// Visit a pattern in parameter position: skip binding idents, but visit
676    /// default values and computed keys as references.
677    fn visit_param_pattern(&mut self, pat: &Pat) {
678        match pat {
679            Pat::Ident(_) => {
680                // Declaration — skip
681            }
682            Pat::Array(arr) => {
683                for elem in &arr.elems {
684                    if let Some(p) = elem {
685                        self.visit_param_pattern(p);
686                    }
687                }
688            }
689            Pat::Object(obj) => {
690                for prop in &obj.props {
691                    match prop {
692                        ObjectPatProp::KeyValue(kv) => {
693                            if let PropName::Computed(computed) = &kv.key {
694                                computed.visit_with(self);
695                            }
696                            self.visit_param_pattern(&kv.value);
697                        }
698                        ObjectPatProp::Assign(assign) => {
699                            if let Some(value) = &assign.value {
700                                value.visit_with(self);
701                            }
702                        }
703                        ObjectPatProp::Rest(rest) => {
704                            self.visit_param_pattern(&rest.arg);
705                        }
706                    }
707                }
708            }
709            Pat::Assign(assign) => {
710                self.visit_param_pattern(&assign.left);
711                // Default value IS a reference
712                assign.right.visit_with(self);
713            }
714            Pat::Rest(rest) => {
715                self.visit_param_pattern(&rest.arg);
716            }
717            Pat::Expr(expr) => {
718                expr.visit_with(self);
719            }
720            Pat::Invalid(_) => {}
721        }
722    }
723
724    /// Visit function internals for the resolver (params + body), mirroring the collector.
725    fn visit_function_inner(&mut self, function: &Function) {
726        let func_start = function.span.lo.0;
727        if let Some(&scope_id) = self.find_scope_at(func_start) {
728            self.scope_stack.push(scope_id);
729
730            for param in &function.params {
731                self.visit_param_pattern(&param.pat);
732            }
733
734            if let Some(body) = &function.body {
735                self.function_body_spans.insert(body.span.lo.0);
736                body.visit_with(self);
737            }
738
739            self.scope_stack.pop();
740        }
741    }
742}
743
744impl<'a> Visit for ReferenceResolver<'a> {
745    fn visit_module(&mut self, module: &Module) {
746        self.scope_stack.push(ScopeId(0));
747        module.visit_children_with(self);
748        self.scope_stack.pop();
749    }
750
751    fn visit_ident(&mut self, ident: &Ident) {
752        let name = ident.sym.to_string();
753        let start = ident.span.lo.0;
754        self.resolve_ident(&name, start);
755    }
756
757    fn visit_import_decl(&mut self, _import: &ImportDecl) {
758        // Don't recurse — import identifiers are declarations
759    }
760
761    fn visit_var_decl(&mut self, var_decl: &VarDecl) {
762        // Only visit initializers, not patterns (which are declarations)
763        for declarator in &var_decl.decls {
764            if let Some(init) = &declarator.init {
765                init.visit_with(self);
766            }
767        }
768    }
769
770    fn visit_fn_decl(&mut self, fn_decl: &FnDecl) {
771        // Don't resolve the function name — it's a declaration
772        self.visit_function_inner(&fn_decl.function);
773    }
774
775    fn visit_export_default_decl(&mut self, decl: &ExportDefaultDecl) {
776        // Mirror the collector: handle exported functions/classes with their own
777        // scope logic, rather than falling through to the default FnExpr visitor.
778        match &decl.decl {
779            DefaultDecl::Fn(fn_expr) => {
780                // Don't resolve the function name — it's a declaration
781                self.visit_function_inner(&fn_expr.function);
782            }
783            DefaultDecl::Class(class_expr) => {
784                if let Some(&scope_id) = self.find_scope_at(class_expr.class.span.lo.0) {
785                    self.scope_stack.push(scope_id);
786                    class_expr.class.visit_children_with(self);
787                    self.scope_stack.pop();
788                }
789            }
790            DefaultDecl::TsInterfaceDecl(d) => {
791                d.visit_with(self);
792            }
793        }
794    }
795
796    fn visit_fn_expr(&mut self, fn_expr: &FnExpr) {
797        let func_start = fn_expr.function.span.lo.0;
798        if let Some(&scope_id) = self.find_scope_at(func_start) {
799            self.scope_stack.push(scope_id);
800
801            // Don't resolve named fn expr ident — it's a declaration
802
803            for param in &fn_expr.function.params {
804                self.visit_param_pattern(&param.pat);
805            }
806
807            if let Some(body) = &fn_expr.function.body {
808                self.function_body_spans.insert(body.span.lo.0);
809                body.visit_with(self);
810            }
811
812            self.scope_stack.pop();
813        }
814    }
815
816    fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) {
817        let func_start = arrow.span.lo.0;
818        if let Some(&scope_id) = self.find_scope_at(func_start) {
819            self.scope_stack.push(scope_id);
820
821            for param in &arrow.params {
822                self.visit_param_pattern(param);
823            }
824
825            match &*arrow.body {
826                BlockStmtOrExpr::BlockStmt(block) => {
827                    self.function_body_spans.insert(block.span.lo.0);
828                    block.visit_with(self);
829                }
830                BlockStmtOrExpr::Expr(expr) => {
831                    expr.visit_with(self);
832                }
833            }
834
835            self.scope_stack.pop();
836        }
837    }
838
839    fn visit_block_stmt(&mut self, block: &BlockStmt) {
840        if self.function_body_spans.remove(&block.span.lo.0) {
841            // Function/catch body — scope already pushed
842            block.visit_children_with(self);
843        } else if let Some(&scope_id) = self.find_scope_at(block.span.lo.0) {
844            self.scope_stack.push(scope_id);
845            block.visit_children_with(self);
846            self.scope_stack.pop();
847        } else {
848            block.visit_children_with(self);
849        }
850    }
851
852    fn visit_for_stmt(&mut self, for_stmt: &ForStmt) {
853        if let Some(&scope_id) = self.find_scope_at(for_stmt.span.lo.0) {
854            self.scope_stack.push(scope_id);
855
856            if let Some(init) = &for_stmt.init {
857                init.visit_with(self);
858            }
859            if let Some(test) = &for_stmt.test {
860                test.visit_with(self);
861            }
862            if let Some(update) = &for_stmt.update {
863                update.visit_with(self);
864            }
865            for_stmt.body.visit_with(self);
866
867            self.scope_stack.pop();
868        }
869    }
870
871    fn visit_for_in_stmt(&mut self, for_in: &ForInStmt) {
872        if let Some(&scope_id) = self.find_scope_at(for_in.span.lo.0) {
873            self.scope_stack.push(scope_id);
874            for_in.left.visit_with(self);
875            for_in.right.visit_with(self);
876            for_in.body.visit_with(self);
877            self.scope_stack.pop();
878        }
879    }
880
881    fn visit_for_of_stmt(&mut self, for_of: &ForOfStmt) {
882        if let Some(&scope_id) = self.find_scope_at(for_of.span.lo.0) {
883            self.scope_stack.push(scope_id);
884            for_of.left.visit_with(self);
885            for_of.right.visit_with(self);
886            for_of.body.visit_with(self);
887            self.scope_stack.pop();
888        }
889    }
890
891    fn visit_catch_clause(&mut self, catch: &CatchClause) {
892        if let Some(&scope_id) = self.find_scope_at(catch.span.lo.0) {
893            self.scope_stack.push(scope_id);
894            // Don't visit catch param — it's a declaration
895            self.function_body_spans.insert(catch.body.span.lo.0);
896            catch.body.visit_with(self);
897            self.scope_stack.pop();
898        }
899    }
900
901    fn visit_switch_stmt(&mut self, switch: &SwitchStmt) {
902        switch.discriminant.visit_with(self);
903
904        if let Some(&scope_id) = self.find_scope_at(switch.span.lo.0) {
905            self.scope_stack.push(scope_id);
906            for case in &switch.cases {
907                case.visit_with(self);
908            }
909            self.scope_stack.pop();
910        }
911    }
912
913    fn visit_class_decl(&mut self, class_decl: &ClassDecl) {
914        // Don't resolve the class name — it's a declaration
915        if let Some(&scope_id) = self.find_scope_at(class_decl.class.span.lo.0) {
916            self.scope_stack.push(scope_id);
917            class_decl.class.visit_children_with(self);
918            self.scope_stack.pop();
919        }
920    }
921
922    fn visit_class_expr(&mut self, class_expr: &ClassExpr) {
923        if let Some(&scope_id) = self.find_scope_at(class_expr.class.span.lo.0) {
924            self.scope_stack.push(scope_id);
925            // Don't resolve named class expr ident — it's a declaration
926            class_expr.class.visit_children_with(self);
927            self.scope_stack.pop();
928        }
929    }
930
931    fn visit_function(&mut self, f: &Function) {
932        // Reached for object/class methods via default traversal
933        self.visit_function_inner(f);
934    }
935
936    // Don't resolve property idents on member expressions as references
937    fn visit_member_expr(&mut self, member: &MemberExpr) {
938        member.obj.visit_with(self);
939        if let MemberProp::Computed(computed) = &member.prop {
940            computed.visit_with(self);
941        }
942    }
943
944    // Handle property definitions — don't resolve non-computed keys
945    fn visit_prop(&mut self, prop: &Prop) {
946        match prop {
947            Prop::Shorthand(ident) => {
948                // Shorthand property `{ x }` — `x` is a reference
949                self.visit_ident(ident);
950            }
951            Prop::KeyValue(kv) => {
952                if let PropName::Computed(computed) = &kv.key {
953                    computed.visit_with(self);
954                }
955                kv.value.visit_with(self);
956            }
957            Prop::Assign(assign) => {
958                assign.value.visit_with(self);
959            }
960            Prop::Getter(getter) => {
961                if let PropName::Computed(computed) = &getter.key {
962                    computed.visit_with(self);
963                }
964                if let Some(body) = &getter.body {
965                    body.visit_with(self);
966                }
967            }
968            Prop::Setter(setter) => {
969                if let PropName::Computed(computed) = &setter.key {
970                    computed.visit_with(self);
971                }
972                setter.param.visit_with(self);
973                if let Some(body) = &setter.body {
974                    body.visit_with(self);
975                }
976            }
977            Prop::Method(method) => {
978                if let PropName::Computed(computed) = &method.key {
979                    computed.visit_with(self);
980                }
981                method.function.visit_with(self);
982            }
983        }
984    }
985
986    // Don't resolve labels
987    fn visit_labeled_stmt(&mut self, labeled: &LabeledStmt) {
988        labeled.body.visit_with(self);
989    }
990
991    fn visit_break_stmt(&mut self, _break_stmt: &BreakStmt) {}
992
993    fn visit_continue_stmt(&mut self, _continue_stmt: &ContinueStmt) {}
994}