Skip to main content

shape_lsp/
scope.rs

1//! Scope-aware symbol resolution for Shape
2//!
3//! Builds a scope tree from the AST for accurate find-references and rename
4//! operations that respect variable shadowing and lexical scoping.
5
6use shape_ast::ast::{BlockItem, Expr, Item, Pattern, Program, Span, Spanned, Statement};
7
8/// A binding (variable/function definition) within a scope.
9#[derive(Debug, Clone)]
10pub struct Binding {
11    /// The identifier name
12    pub name: String,
13    /// Byte span of the definition site
14    pub def_span: (usize, usize),
15    /// Byte spans of all reference sites (excludes the definition itself)
16    pub references: Vec<(usize, usize)>,
17}
18
19/// A single lexical scope.
20#[derive(Debug)]
21pub struct Scope {
22    /// Byte range this scope covers
23    pub range: (usize, usize),
24    /// Parent scope index (None for the module scope)
25    pub parent: Option<usize>,
26    /// Bindings introduced in this scope
27    pub bindings: Vec<Binding>,
28    /// Child scope indices
29    pub children: Vec<usize>,
30}
31
32/// A tree of lexical scopes built from a parsed program.
33#[derive(Debug)]
34pub struct ScopeTree {
35    pub scopes: Vec<Scope>,
36}
37
38impl ScopeTree {
39    /// Build a scope tree from a parsed program.
40    pub fn build(program: &Program, source: &str) -> Self {
41        let mut tree = ScopeTree { scopes: Vec::new() };
42
43        // Create the module (root) scope covering the entire source
44        let root = tree.push_scope(0, source.len(), None);
45
46        // First pass: collect all definitions
47        for item in &program.items {
48            tree.collect_item_definitions(item, root);
49        }
50
51        // Second pass: collect all identifier references
52        for item in &program.items {
53            tree.collect_item_references(item, root);
54        }
55
56        tree
57    }
58
59    /// Find all references to the binding at the given byte offset.
60    ///
61    /// Returns the definition span + all reference spans, or None if no binding
62    /// is found at the offset.
63    pub fn references_of(&self, offset: usize) -> Option<Vec<(usize, usize)>> {
64        // Find which binding the offset falls within
65        let binding = self.find_binding_at(offset)?;
66
67        let mut result = vec![binding.def_span];
68        result.extend_from_slice(&binding.references);
69        Some(result)
70    }
71
72    /// Find the definition span of the binding at the given byte offset.
73    pub fn definition_of(&self, offset: usize) -> Option<(usize, usize)> {
74        let binding = self.find_binding_at(offset)?;
75        Some(binding.def_span)
76    }
77
78    /// Return the binding (definition + references) visible at the given
79    /// offset. Used by hover to resolve shadowed variables correctly.
80    pub fn binding_at(&self, offset: usize) -> Option<&Binding> {
81        self.find_binding_at(offset)
82    }
83
84    // --- Internal helpers ---
85
86    fn push_scope(&mut self, start: usize, end: usize, parent: Option<usize>) -> usize {
87        let idx = self.scopes.len();
88        self.scopes.push(Scope {
89            range: (start, end),
90            parent,
91            bindings: Vec::new(),
92            children: Vec::new(),
93        });
94        if let Some(parent_idx) = parent {
95            self.scopes[parent_idx].children.push(idx);
96        }
97        idx
98    }
99
100    fn add_binding(&mut self, scope_idx: usize, name: String, def_span: (usize, usize)) {
101        self.scopes[scope_idx].bindings.push(Binding {
102            name,
103            def_span,
104            references: Vec::new(),
105        });
106    }
107
108    /// Find a binding at the given byte offset.
109    /// Checks definitions and references in all scopes.
110    fn find_binding_at(&self, offset: usize) -> Option<&Binding> {
111        for scope in &self.scopes {
112            for binding in &scope.bindings {
113                // Check if offset is on the definition
114                if offset >= binding.def_span.0 && offset < binding.def_span.1 {
115                    return Some(binding);
116                }
117                // Check if offset is on any reference
118                for &(start, end) in &binding.references {
119                    if offset >= start && offset < end {
120                        return Some(binding);
121                    }
122                }
123            }
124        }
125        None
126    }
127
128    /// Resolve a name in the given scope, walking up to parent scopes.
129    fn resolve_name(&self, name: &str, scope_idx: usize) -> Option<(usize, usize)> {
130        let scope = &self.scopes[scope_idx];
131        // Search current scope bindings (last binding with this name wins — shadowing)
132        for binding in scope.bindings.iter().rev() {
133            if binding.name == name {
134                return Some((
135                    scope_idx,
136                    scope.bindings.iter().rposition(|b| b.name == name).unwrap(),
137                ));
138            }
139        }
140        // Walk up to parent
141        if let Some(parent) = scope.parent {
142            self.resolve_name(name, parent)
143        } else {
144            None
145        }
146    }
147
148    /// Add a reference to a binding (if it exists in scope).
149    fn add_reference(&mut self, name: &str, ref_span: (usize, usize), scope_idx: usize) {
150        if let Some((scope_id, binding_id)) = self.resolve_name(name, scope_idx) {
151            self.scopes[scope_id].bindings[binding_id]
152                .references
153                .push(ref_span);
154        }
155    }
156
157    /// Find the innermost scope containing the given byte offset.
158    fn scope_at(&self, offset: usize) -> usize {
159        let mut best = 0; // root scope
160        for (idx, scope) in self.scopes.iter().enumerate() {
161            if offset >= scope.range.0
162                && offset < scope.range.1
163                && (scope.range.1 - scope.range.0)
164                    < (self.scopes[best].range.1 - self.scopes[best].range.0)
165            {
166                best = idx;
167            }
168        }
169        best
170    }
171
172    // --- Definition collection ---
173
174    fn collect_item_definitions(&mut self, item: &Item, scope_idx: usize) {
175        match item {
176            Item::Function(func, _span) => {
177                // The function name is defined in the outer scope
178                let name_span = func.name_span;
179                if !name_span.is_dummy() {
180                    self.add_binding(
181                        scope_idx,
182                        func.name.clone(),
183                        (name_span.start, name_span.end),
184                    );
185                }
186
187                // Create a new scope for the function body
188                let func_span = _span;
189                let func_scope = self.push_scope(func_span.start, func_span.end, Some(scope_idx));
190
191                // Parameters are bindings in the function scope
192                for param in &func.params {
193                    if let Some(name) = param.simple_name() {
194                        let ps = param.span();
195                        if !ps.is_dummy() {
196                            self.add_binding(func_scope, name.to_string(), (ps.start, ps.end));
197                        }
198                    }
199                }
200
201                // Walk the body
202                for stmt in &func.body {
203                    self.collect_stmt_definitions(stmt, func_scope);
204                }
205            }
206            Item::VariableDecl(decl, span) => {
207                self.collect_var_decl_def(decl, span, scope_idx);
208            }
209            Item::Statement(stmt, _) => {
210                self.collect_stmt_definitions(stmt, scope_idx);
211            }
212            // Types, enums, traits, etc. define names in the module scope
213            Item::StructType(s, span) => {
214                if !span.is_dummy() {
215                    self.add_binding(
216                        scope_idx,
217                        s.name.clone(),
218                        (span.start, span.start + s.name.len()),
219                    );
220                }
221            }
222            Item::Enum(e, span) => {
223                if !span.is_dummy() {
224                    self.add_binding(
225                        scope_idx,
226                        e.name.clone(),
227                        (span.start, span.start + e.name.len()),
228                    );
229                }
230            }
231            Item::Trait(t, span) => {
232                if !span.is_dummy() {
233                    self.add_binding(
234                        scope_idx,
235                        t.name.clone(),
236                        (span.start, span.start + t.name.len()),
237                    );
238                }
239            }
240            Item::TypeAlias(ta, span) => {
241                if !span.is_dummy() {
242                    self.add_binding(
243                        scope_idx,
244                        ta.name.clone(),
245                        (span.start, span.start + ta.name.len()),
246                    );
247                }
248            }
249            Item::ForeignFunction(foreign_fn, _span) => {
250                let name_span = foreign_fn.name_span;
251                if !name_span.is_dummy() {
252                    self.add_binding(
253                        scope_idx,
254                        foreign_fn.name.clone(),
255                        (name_span.start, name_span.end),
256                    );
257                }
258            }
259            _ => {}
260        }
261    }
262
263    fn collect_stmt_definitions(&mut self, stmt: &Statement, scope_idx: usize) {
264        match stmt {
265            Statement::VariableDecl(decl, span) => {
266                self.collect_var_decl_def(decl, span, scope_idx);
267            }
268            Statement::Expression(expr, _) => {
269                self.collect_expr_definitions(expr, scope_idx);
270            }
271            _ => {}
272        }
273    }
274
275    fn collect_var_decl_def(
276        &mut self,
277        decl: &shape_ast::ast::VariableDecl,
278        _span: &Span,
279        scope_idx: usize,
280    ) {
281        for (name, span) in crate::symbols::get_pattern_names(&decl.pattern) {
282            if !span.is_dummy() {
283                self.add_binding(scope_idx, name, (span.start, span.end));
284            }
285        }
286
287        // Walk value expression for nested definitions
288        if let Some(value) = &decl.value {
289            self.collect_expr_definitions(value, scope_idx);
290        }
291    }
292
293    fn collect_expr_definitions(&mut self, expr: &Expr, scope_idx: usize) {
294        match expr {
295            Expr::Block(block, span) => {
296                let block_scope = self.push_scope(span.start, span.end, Some(scope_idx));
297                for item in &block.items {
298                    match item {
299                        BlockItem::Statement(stmt) => {
300                            self.collect_stmt_definitions(stmt, block_scope);
301                        }
302                        BlockItem::Expression(e) => {
303                            self.collect_expr_definitions(e, block_scope);
304                        }
305                        BlockItem::VariableDecl(vd) => {
306                            self.collect_stmt_definitions(
307                                &shape_ast::ast::Statement::VariableDecl(
308                                    vd.clone(),
309                                    shape_ast::ast::Span::DUMMY,
310                                ),
311                                block_scope,
312                            );
313                        }
314                        BlockItem::Assignment(_) => {
315                            // Assignments don't introduce new definitions
316                        }
317                    }
318                }
319            }
320            Expr::For(for_expr, span) => {
321                let for_scope = self.push_scope(span.start, span.end, Some(scope_idx));
322                // Loop variable from pattern
323                if let Pattern::Identifier(name) = &for_expr.pattern {
324                    // Use the span start to approximate the variable position
325                    // (Pattern doesn't carry its own span, use the for-expr span start)
326                    let name_start = span.start;
327                    let name_end = name_start + name.len();
328                    self.add_binding(for_scope, name.clone(), (name_start, name_end));
329                }
330                self.collect_expr_definitions(&for_expr.body, for_scope);
331            }
332            Expr::If(if_expr, _) => {
333                self.collect_expr_definitions(&if_expr.then_branch, scope_idx);
334                if let Some(else_branch) = &if_expr.else_branch {
335                    self.collect_expr_definitions(else_branch, scope_idx);
336                }
337            }
338            Expr::FunctionExpr {
339                body, params, span, ..
340            } => {
341                let closure_scope = self.push_scope(span.start, span.end, Some(scope_idx));
342                for param in params {
343                    if let Some(name) = param.simple_name() {
344                        let ps = param.span();
345                        if !ps.is_dummy() {
346                            self.add_binding(closure_scope, name.to_string(), (ps.start, ps.end));
347                        }
348                    }
349                }
350                for stmt in body {
351                    self.collect_stmt_definitions(stmt, closure_scope);
352                }
353            }
354            Expr::Match(match_expr, _span) => {
355                for arm in &match_expr.arms {
356                    // Each arm creates a scope for pattern bindings
357                    let arm_span = (*arm.body).span();
358                    if !arm_span.is_dummy() {
359                        let arm_scope =
360                            self.push_scope(arm_span.start, arm_span.end, Some(scope_idx));
361                        self.collect_pattern_bindings(
362                            &arm.pattern,
363                            arm.pattern_span.as_ref(),
364                            arm_scope,
365                        );
366                        self.collect_expr_definitions(&arm.body, arm_scope);
367                    }
368                }
369            }
370            _ => {}
371        }
372    }
373
374    fn collect_pattern_bindings(
375        &mut self,
376        pattern: &shape_ast::ast::Pattern,
377        pattern_span: Option<&Span>,
378        scope_idx: usize,
379    ) {
380        match pattern {
381            shape_ast::ast::Pattern::Identifier(name) => {
382                if let Some(span) = pattern_span {
383                    if !span.is_dummy() {
384                        let start = span.start;
385                        let end = start.saturating_add(name.len());
386                        self.add_binding(scope_idx, name.clone(), (start, end));
387                    }
388                }
389            }
390            shape_ast::ast::Pattern::Typed {
391                name,
392                type_annotation: _,
393            } => {
394                if let Some(span) = pattern_span {
395                    if !span.is_dummy() {
396                        let start = span.start;
397                        let end = start.saturating_add(name.len());
398                        self.add_binding(scope_idx, name.clone(), (start, end));
399                    }
400                }
401            }
402            shape_ast::ast::Pattern::Constructor { .. } => {
403                // Recurse into constructor pattern fields
404            }
405            _ => {}
406        }
407    }
408
409    // --- Reference collection ---
410
411    fn collect_item_references(&mut self, item: &Item, scope_idx: usize) {
412        match item {
413            Item::Function(func, span) => {
414                let func_scope = self.find_child_scope(scope_idx, span);
415                for stmt in &func.body {
416                    self.collect_stmt_references(stmt, func_scope);
417                }
418            }
419            Item::VariableDecl(decl, _) => {
420                if let Some(value) = &decl.value {
421                    self.collect_expr_references(value, scope_idx);
422                }
423            }
424            Item::Statement(stmt, _) => {
425                self.collect_stmt_references(stmt, scope_idx);
426            }
427            Item::Expression(expr, _) => {
428                self.collect_expr_references(expr, scope_idx);
429            }
430            _ => {}
431        }
432    }
433
434    fn collect_stmt_references(&mut self, stmt: &Statement, scope_idx: usize) {
435        match stmt {
436            Statement::VariableDecl(decl, _) => {
437                if let Some(value) = &decl.value {
438                    self.collect_expr_references(value, scope_idx);
439                }
440            }
441            Statement::Expression(expr, _) => {
442                self.collect_expr_references(expr, scope_idx);
443            }
444            Statement::Return(Some(expr), _) => {
445                self.collect_expr_references(expr, scope_idx);
446            }
447            _ => {}
448        }
449    }
450
451    fn collect_expr_references(&mut self, expr: &Expr, scope_idx: usize) {
452        match expr {
453            Expr::Identifier(name, span) => {
454                if !span.is_dummy() {
455                    self.add_reference(name, (span.start, span.end), scope_idx);
456                }
457            }
458            Expr::Block(block, span) => {
459                let block_scope = self.find_child_scope(scope_idx, span);
460                for item in &block.items {
461                    match item {
462                        BlockItem::Statement(stmt) => {
463                            self.collect_stmt_references(stmt, block_scope);
464                        }
465                        BlockItem::Expression(e) => {
466                            self.collect_expr_references(e, block_scope);
467                        }
468                        BlockItem::VariableDecl(vd) => {
469                            self.collect_stmt_references(
470                                &shape_ast::ast::Statement::VariableDecl(
471                                    vd.clone(),
472                                    shape_ast::ast::Span::DUMMY,
473                                ),
474                                block_scope,
475                            );
476                        }
477                        BlockItem::Assignment(assign) => {
478                            self.collect_expr_references(&assign.value, block_scope);
479                        }
480                    }
481                }
482            }
483            Expr::For(for_expr, span) => {
484                let for_scope = self.find_child_scope(scope_idx, span);
485                self.collect_expr_references(&for_expr.iterable, scope_idx);
486                self.collect_expr_references(&for_expr.body, for_scope);
487            }
488            Expr::If(if_expr, _) => {
489                self.collect_expr_references(&if_expr.condition, scope_idx);
490                self.collect_expr_references(&if_expr.then_branch, scope_idx);
491                if let Some(else_branch) = &if_expr.else_branch {
492                    self.collect_expr_references(else_branch, scope_idx);
493                }
494            }
495            Expr::BinaryOp { left, right, .. } => {
496                self.collect_expr_references(left, scope_idx);
497                self.collect_expr_references(right, scope_idx);
498            }
499            Expr::UnaryOp { operand, .. } => {
500                self.collect_expr_references(operand, scope_idx);
501            }
502            Expr::FunctionCall {
503                name, args, span, ..
504            } => {
505                // The function name is a reference
506                if !span.is_dummy() {
507                    // Find the start of the function name in the source
508                    let name_start = span.start;
509                    let name_end = name_start + name.len();
510                    self.add_reference(name, (name_start, name_end), scope_idx);
511                }
512                for arg in args {
513                    self.collect_expr_references(arg, scope_idx);
514                }
515            }
516            Expr::MethodCall { receiver, args, .. } => {
517                self.collect_expr_references(receiver, scope_idx);
518                for arg in args {
519                    self.collect_expr_references(arg, scope_idx);
520                }
521            }
522            Expr::PropertyAccess { object, .. } => {
523                self.collect_expr_references(object, scope_idx);
524            }
525            Expr::IndexAccess { object, index, .. } => {
526                self.collect_expr_references(object, scope_idx);
527                self.collect_expr_references(index, scope_idx);
528            }
529            Expr::Array(elements, _) => {
530                for el in elements {
531                    self.collect_expr_references(el, scope_idx);
532                }
533            }
534            Expr::Object(entries, _) => {
535                for entry in entries {
536                    if let shape_ast::ast::ObjectEntry::Field { value, .. } = entry {
537                        self.collect_expr_references(value, scope_idx);
538                    }
539                }
540            }
541            Expr::Assign(assign, _) => {
542                self.collect_expr_references(&assign.target, scope_idx);
543                self.collect_expr_references(&assign.value, scope_idx);
544            }
545            Expr::Return(Some(inner), _) => {
546                self.collect_expr_references(inner, scope_idx);
547            }
548            Expr::Conditional {
549                condition,
550                then_expr,
551                else_expr,
552                ..
553            } => {
554                self.collect_expr_references(condition, scope_idx);
555                self.collect_expr_references(then_expr, scope_idx);
556                if let Some(else_e) = else_expr {
557                    self.collect_expr_references(else_e, scope_idx);
558                }
559            }
560            Expr::Match(match_expr, _span) => {
561                self.collect_expr_references(&match_expr.scrutinee, scope_idx);
562                for arm in &match_expr.arms {
563                    let arm_span = (*arm.body).span();
564                    let arm_scope = if !arm_span.is_dummy() {
565                        self.find_child_scope(scope_idx, &arm_span)
566                    } else {
567                        scope_idx
568                    };
569                    self.collect_expr_references(&arm.body, arm_scope);
570                }
571            }
572            Expr::Literal(_, _) => {}
573            Expr::FunctionExpr { body, span, .. } => {
574                let closure_scope = self.find_child_scope(scope_idx, span);
575                for stmt in body {
576                    self.collect_stmt_references(stmt, closure_scope);
577                }
578            }
579            Expr::Await(inner, _) => {
580                self.collect_expr_references(inner, scope_idx);
581            }
582            Expr::TryOperator(inner, _) => {
583                self.collect_expr_references(inner, scope_idx);
584            }
585            Expr::TypeAssertion { expr: inner, .. } => {
586                self.collect_expr_references(inner, scope_idx);
587            }
588            Expr::Spread(inner, _) => {
589                self.collect_expr_references(inner, scope_idx);
590            }
591            Expr::StructLiteral { fields, .. } => {
592                for (_, value) in fields {
593                    self.collect_expr_references(value, scope_idx);
594                }
595            }
596            _ => {
597                // For other expression types, we don't recurse deeper
598            }
599        }
600    }
601
602    /// Find a child scope of `parent_idx` that matches the given span.
603    /// Falls back to `parent_idx` if no matching child is found.
604    fn find_child_scope(&self, parent_idx: usize, span: &Span) -> usize {
605        for &child in &self.scopes[parent_idx].children {
606            let cs = &self.scopes[child];
607            if cs.range.0 == span.start && cs.range.1 == span.end {
608                return child;
609            }
610        }
611        // Fallback: find the innermost scope containing this span
612        self.scope_at(span.start)
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use shape_ast::parser::parse_program;
620
621    #[test]
622    fn test_scope_tree_basic_variable() {
623        let code = "let x = 42\nlet y = x + 1";
624        let program = parse_program(code).unwrap();
625        let tree = ScopeTree::build(&program, code);
626
627        // Root scope should exist
628        assert!(!tree.scopes.is_empty());
629
630        // x should have 1 reference (the use in `y = x + 1`)
631        let x_binding = tree.scopes[0].bindings.iter().find(|b| b.name == "x");
632        assert!(x_binding.is_some(), "Should find binding for 'x'");
633        let x = x_binding.unwrap();
634        assert!(
635            !x.references.is_empty(),
636            "x should have at least one reference, got: {:?}",
637            x
638        );
639    }
640
641    #[test]
642    fn test_scope_tree_function_scope() {
643        let code = "let x = 1\nfn foo(a) {\n  let b = a + x\n  return b\n}";
644        let program = parse_program(code).unwrap();
645        let tree = ScopeTree::build(&program, code);
646
647        // Should have at least 2 scopes: root + function body
648        assert!(
649            tree.scopes.len() >= 2,
650            "Should have root + function scope, got {}",
651            tree.scopes.len()
652        );
653
654        // `a` should be bound in the function scope
655        let func_scope = &tree.scopes[1];
656        let a_binding = func_scope.bindings.iter().find(|b| b.name == "a");
657        assert!(a_binding.is_some(), "Should find 'a' in function scope");
658    }
659
660    #[test]
661    fn test_scope_tree_shadowing() {
662        let code = "let x = 1\nfn foo() {\n  let x = 2\n  return x\n}";
663        let program = parse_program(code).unwrap();
664        let tree = ScopeTree::build(&program, code);
665
666        // There should be two bindings named "x" in different scopes
667        let x_count: usize = tree
668            .scopes
669            .iter()
670            .flat_map(|s| &s.bindings)
671            .filter(|b| b.name == "x")
672            .count();
673        assert!(
674            x_count >= 2,
675            "Should have at least 2 bindings named 'x' (shadowing), got {}",
676            x_count
677        );
678    }
679
680    #[test]
681    fn test_references_of() {
682        let code = "let x = 1\nlet y = x";
683        let program = parse_program(code).unwrap();
684        let tree = ScopeTree::build(&program, code);
685
686        // Find the definition of x
687        let x_binding = tree.scopes[0].bindings.iter().find(|b| b.name == "x");
688        assert!(x_binding.is_some());
689        let x = x_binding.unwrap();
690
691        // references_of should return def + references
692        let refs = tree.references_of(x.def_span.0);
693        assert!(refs.is_some(), "Should find references for x");
694        let refs = refs.unwrap();
695        assert!(
696            refs.len() >= 2,
697            "x should have def + at least 1 reference, got {}",
698            refs.len()
699        );
700    }
701
702    #[test]
703    fn test_shadowing_does_not_cross_scopes() {
704        // Inner x should NOT reference outer x
705        let code = "let x = 1\nfn foo() {\n  let x = 2\n  return x\n}\nlet z = x";
706        let program = parse_program(code).unwrap();
707        let tree = ScopeTree::build(&program, code);
708
709        // The outer x (at root scope) should have a reference in `z = x`
710        // but NOT from the inner `return x`
711        let outer_x = tree.scopes[0].bindings.iter().find(|b| b.name == "x");
712        assert!(outer_x.is_some(), "Should find outer x");
713        let outer_x = outer_x.unwrap();
714
715        // The outer x's references should include `z = x` but not the inner `return x`
716        // Since inner x shadows, `return x` binds to inner x, not outer
717        let outer_refs = tree.references_of(outer_x.def_span.0).unwrap();
718        // Outer x def + reference in `z = x` = 2
719        assert_eq!(
720            outer_refs.len(),
721            2,
722            "Outer x should have exactly 2 spans (def + z=x ref), got {}",
723            outer_refs.len()
724        );
725    }
726
727    #[test]
728    fn test_definition_of() {
729        let code = "let myVar = 42\nlet result = myVar + 1";
730        let program = parse_program(code).unwrap();
731        let tree = ScopeTree::build(&program, code);
732
733        // Find the reference to myVar in `myVar + 1`
734        let ref_offset = code.rfind("myVar").unwrap();
735        let def = tree.definition_of(ref_offset);
736        assert!(def.is_some(), "Should find definition of myVar");
737
738        let (start, end) = def.unwrap();
739        assert_eq!(
740            &code[start..end],
741            "myVar",
742            "Definition should point to 'myVar'"
743        );
744    }
745
746    #[test]
747    fn test_closure_scope() {
748        let code = "let x = 1\nlet f = |y| y + x";
749        let program = parse_program(code).unwrap();
750        let tree = ScopeTree::build(&program, code);
751
752        // x from the outer scope should be referenced inside the closure
753        let outer_x = tree.scopes[0].bindings.iter().find(|b| b.name == "x");
754        assert!(outer_x.is_some(), "Should find outer x");
755        let outer_x = outer_x.unwrap();
756        assert!(
757            !outer_x.references.is_empty(),
758            "Outer x should be referenced from inside closure"
759        );
760    }
761}