Skip to main content

sem_core/parser/
scope_resolve.rs

1//! Scope-aware reference resolver using tree-sitter ASTs.
2//!
3//! Instead of bag-of-words tokenization (current graph.rs Pass 2), this module
4//! walks the tree-sitter AST to find actual reference nodes (calls, attribute access)
5//! and resolves them using scope chains. This gives compiler-like accuracy for
6//! name resolution without needing a full language server.
7//!
8//! Key improvements over bag-of-words:
9//! - Distinguishes definitions from references in the AST
10//! - Resolves same-name entities via scope chains (no false collisions)
11//! - Tracks variable types through assignments (x = Foo() → x.method → Foo.method)
12//! - Uses AST structure, not string matching
13
14use std::collections::HashMap;
15use std::path::Path;
16use std::sync::Arc;
17
18use rayon::prelude::*;
19
20use crate::model::entity::SemanticEntity;
21use crate::parser::graph::{EntityInfo, RefType};
22use crate::parser::plugins::code::languages::{
23    get_language_config, AssignmentStrategy, CallNodeStyle, ClassNameField, InitStrategy,
24    ParamNameField, ScopeResolveConfig,
25};
26
27/// A scope in the scope tree. Scopes are nested: module -> class -> function -> block.
28pub struct Scope {
29    parent: Option<usize>,
30    /// Definitions visible in this scope: name -> entity_id
31    defs: HashMap<String, String>,
32    /// Variable type bindings: var_name -> class_name (from `x = Foo()`)
33    types: HashMap<String, String>,
34    /// Unresolved call assignments: var_name -> function_name (from `x = func()`)
35    /// These get resolved after return type analysis.
36    pending_call_types: HashMap<String, String>,
37    /// Which entity owns this scope (if any)
38    owner_id: Option<String>,
39    /// What kind of scope: "module", "class", "function"
40    kind: &'static str,
41}
42
43/// Reference found in the AST
44struct AstRef {
45    /// Kind of reference
46    kind: AstRefKind,
47    /// Row (0-indexed) where this reference appears in the source
48    row: usize,
49}
50
51enum AstRefKind {
52    /// Bare name call: `foo()`
53    Call(String),
54    /// Attribute call: `x.method()`
55    MethodCall { receiver: String, method: String },
56}
57
58/// Result of scope-aware resolution
59pub struct ScopeResult {
60    pub edges: Vec<(String, String, RefType)>,
61    /// Debug info: which references were resolved and how
62    pub resolution_log: Vec<ResolutionEntry>,
63}
64
65#[derive(Clone)]
66pub struct ResolutionEntry {
67    pub from_entity: String,
68    pub reference: String,
69    pub resolved_to: Option<String>,
70    pub method: &'static str, // "scope_chain", "type_tracking", "import", "unresolved"
71}
72
73/// Resolve references using tree-sitter scope analysis.
74///
75/// For each file:
76/// 1. Parse with tree-sitter
77/// 2. Build a scope tree (module -> class -> function)
78/// 3. Walk entity AST subtrees to find reference nodes
79/// 4. Resolve each reference via scope chain + type tracking
80/// Pre-built lookup tables that can be shared between `EntityGraph::build()` and
81/// `resolve_with_scopes()` to avoid redundant O(E) passes.
82pub(crate) struct PreBuiltLookups {
83    pub(crate) symbol_table: Arc<HashMap<String, Vec<String>>>,
84    pub(crate) class_members: HashMap<String, Vec<(String, String)>>,
85    pub(crate) entity_ranges: HashMap<String, Vec<(usize, usize, String)>>,
86    /// Go package index: pkg_name → [(entity_name, entity_id)]
87    /// Avoids O(symbol_table) scan per Go import.
88    pub(crate) go_pkg_index: HashMap<String, Vec<(String, String)>>,
89}
90
91/// Public API — preserves the original 5-parameter signature for semver compatibility.
92pub fn resolve_with_scopes(
93    root: &Path,
94    file_paths: &[String],
95    all_entities: &[SemanticEntity],
96    entity_map: &HashMap<String, EntityInfo>,
97    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
98) -> ScopeResult {
99    resolve_with_scopes_full(root, file_paths, all_entities, entity_map, pre_parsed, None)
100}
101
102/// Internal version with pre-built lookups for performance.
103pub(crate) fn resolve_with_scopes_full(
104    root: &Path,
105    file_paths: &[String],
106    all_entities: &[SemanticEntity],
107    entity_map: &HashMap<String, EntityInfo>,
108    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
109    pre_built: Option<PreBuiltLookups>,
110) -> ScopeResult {
111    let mut all_edges: Vec<(String, String, RefType)> = Vec::new();
112    let mut log: Vec<ResolutionEntry> = Vec::new();
113
114    // Use pre-built lookups if provided, otherwise build from scratch
115    let (symbol_table, class_members, entity_ranges, go_pkg_index) = if let Some(pb) = pre_built {
116        (pb.symbol_table, pb.class_members, pb.entity_ranges, pb.go_pkg_index)
117    } else {
118        let mut symbol_table: HashMap<String, Vec<String>> = HashMap::new();
119        let mut class_members: HashMap<String, Vec<(String, String)>> = HashMap::new();
120        let mut entity_ranges: HashMap<String, Vec<(usize, usize, String)>> = HashMap::new();
121
122        for entity in all_entities {
123            symbol_table
124                .entry(entity.name.clone())
125                .or_default()
126                .push(entity.id.clone());
127
128            if let Some(ref pid) = entity.parent_id {
129                if let Some(parent) = entity_map.get(pid) {
130                    if matches!(
131                        parent.entity_type.as_str(),
132                        "class" | "struct" | "interface" | "impl"
133                    ) {
134                        class_members
135                            .entry(parent.name.clone())
136                            .or_default()
137                            .push((entity.name.clone(), entity.id.clone()));
138                    }
139                }
140            }
141
142            if entity.entity_type == "method" && entity.file_path.ends_with(".go") {
143                if let Some(struct_name) = extract_go_receiver_type(&entity.content) {
144                    class_members
145                        .entry(struct_name)
146                        .or_default()
147                        .push((entity.name.clone(), entity.id.clone()));
148                }
149            }
150
151            entity_ranges
152                .entry(entity.file_path.clone())
153                .or_default()
154                .push((entity.start_line, entity.end_line, entity.id.clone()));
155        }
156
157        // Build Go package index for O(1) import lookup
158        let go_pkg_index = build_go_pkg_index(&symbol_table, entity_map);
159
160        (Arc::new(symbol_table), class_members, entity_ranges, go_pkg_index)
161    };
162
163    // Build file-path indexed entity lookup: file_path -> Vec<&SemanticEntity>
164    let mut entities_by_file: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
165    for entity in all_entities {
166        entities_by_file
167            .entry(entity.file_path.as_str())
168            .or_default()
169            .push(entity);
170    }
171
172    // Build parent_id indexed entity lookup: parent_id -> Vec<&SemanticEntity>
173    let mut children_by_parent: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
174    for entity in all_entities {
175        if let Some(ref pid) = entity.parent_id {
176            children_by_parent
177                .entry(pid.as_str())
178                .or_default()
179                .push(entity);
180        }
181    }
182
183    // Return type map: function_entity_id -> class_name (if function returns ClassName())
184    let mut return_type_map: HashMap<String, String> = HashMap::new();
185
186    // Instance attribute types: (class_name, attr_name) -> class_name_of_attr
187    let mut instance_attr_types: HashMap<(String, String), String> = HashMap::new();
188
189    // __init__ param info: class_name -> (ordered_params, attr_to_param mapping)
190    // attr_to_param: attr_name -> param_name (for self.attr = param patterns)
191    let mut init_params: HashMap<String, Vec<String>> = HashMap::new();
192    let mut attr_to_param: HashMap<(String, String), String> = HashMap::new();
193
194    // Merge pre-parsed trees with disk-parsed trees for missing files
195    let mut owned_parsed_files: Vec<(String, String, tree_sitter::Tree)> = Vec::new();
196    let pre_set: std::collections::HashSet<String> = if let Some(pp) = pre_parsed {
197        let set = pp.iter().map(|(fp, _, _)| fp.clone()).collect();
198        owned_parsed_files = pp;
199        set
200    } else {
201        std::collections::HashSet::new()
202    };
203    // Parse any files not already in the pre-parsed set
204    for file_path in file_paths {
205        if pre_set.contains(file_path) {
206            continue;
207        }
208        let full_path = root.join(file_path);
209        let content = match std::fs::read_to_string(&full_path) {
210            Ok(c) => c,
211            Err(_) => continue,
212        };
213        let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
214        let config = match get_language_config(ext) {
215            Some(c) => c,
216            None => continue,
217        };
218        let language = match (config.get_language)() {
219            Some(l) => l,
220            None => continue,
221        };
222        let mut parser = tree_sitter::Parser::new();
223        let _ = parser.set_language(&language);
224        if let Some(tree) = parser.parse(content.as_bytes(), None) {
225            owned_parsed_files.push((file_path.clone(), content, tree));
226        }
227    }
228    let parsed_files: &[(String, String, tree_sitter::Tree)] = &owned_parsed_files;
229
230    // Pass 1: Scan ALL files for return types and instance attr types first
231    // This ensures cross-file return type info is available during resolution
232    // Parallelized: each file produces local maps, then merged sequentially.
233    let pass1_results: Vec<(
234        HashMap<String, String>,
235        HashMap<(String, String), String>,
236        HashMap<String, Vec<String>>,
237        HashMap<(String, String), String>,
238    )> = parsed_files
239        .par_iter()
240        .filter_map(|(file_path, content, tree)| {
241            let source = content.as_bytes();
242            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
243            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
244
245            let file_entities = entities_by_file.get(file_path.as_str()).map(|v| v.as_slice()).unwrap_or(&[]);
246
247            let mut local_return_type_map: HashMap<String, String> = HashMap::new();
248            scan_return_types(
249                tree.root_node(),
250                file_path,
251                file_entities,
252                source,
253                &mut local_return_type_map,
254                config,
255            );
256
257            let mut local_instance_attr_types: HashMap<(String, String), String> = HashMap::new();
258            let mut local_init_params: HashMap<String, Vec<String>> = HashMap::new();
259            let mut local_attr_to_param: HashMap<(String, String), String> = HashMap::new();
260            scan_init_self_attrs(
261                tree.root_node(),
262                file_path,
263                file_entities,
264                entity_map,
265                source,
266                &mut local_instance_attr_types,
267                &mut local_init_params,
268                &mut local_attr_to_param,
269                config,
270            );
271
272            Some((local_return_type_map, local_instance_attr_types, local_init_params, local_attr_to_param))
273        })
274        .collect();
275
276    for (local_rtm, local_iat, local_ip, local_atp) in pass1_results {
277        return_type_map.extend(local_rtm);
278        instance_attr_types.extend(local_iat);
279        init_params.extend(local_ip);
280        attr_to_param.extend(local_atp);
281    }
282
283    // Pass 1b: Infer constructor parameter types from call sites
284    // For `Transaction(get_connection())`, infer conn param has type Connection.
285    // Then resolve self.conn = conn -> (Transaction, conn) -> Connection
286    infer_constructor_param_types(
287        parsed_files,
288        &return_type_map,
289        &init_params,
290        &attr_to_param,
291        &symbol_table,
292        entity_map,
293        &mut instance_attr_types,
294    );
295
296    // Pass 2: Build scopes, imports, and resolve references per file (parallel)
297    let per_file_results: Vec<(Vec<(String, String, RefType)>, Vec<ResolutionEntry>)> = parsed_files
298        .par_iter()
299        .filter_map(|(file_path, content, tree)| {
300            let source = content.as_bytes();
301            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
302            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
303
304            let mut scopes: Vec<Scope> = vec![Scope {
305                parent: None,
306                defs: HashMap::new(),
307                types: HashMap::new(),
308                pending_call_types: HashMap::new(),
309                owner_id: None,
310                kind: "module",
311            }];
312
313            let mut entity_scope_map: HashMap<String, usize> = HashMap::new();
314            let mut entity_inner_scope: HashMap<String, usize> = HashMap::new();
315
316            if let Some(ranges) = entity_ranges.get(file_path.as_str()) {
317                for (_start, _end, eid) in ranges {
318                    if let Some(info) = entity_map.get(eid) {
319                        if info.parent_id.is_none() {
320                            scopes[0].defs.insert(info.name.clone(), eid.clone());
321                            entity_scope_map.insert(eid.clone(), 0);
322                        }
323                    }
324                }
325            }
326
327            let file_entities: Vec<&SemanticEntity> = entities_by_file
328                .get(file_path.as_str())
329                .map(|v| v.as_slice())
330                .unwrap_or(&[])
331                .to_vec();
332
333            build_scopes_from_ast(
334                tree.root_node(),
335                0,
336                &mut scopes,
337                &mut entity_scope_map,
338                &mut entity_inner_scope,
339                &file_entities,
340                &children_by_parent,
341                entity_map,
342                file_path,
343                source,
344                config,
345            );
346
347            let mut local_import_table: HashMap<(String, String), String> = HashMap::new();
348            extract_imports_from_ast(
349                tree.root_node(),
350                file_path,
351                source,
352                &symbol_table,
353                entity_map,
354                &mut local_import_table,
355                &mut scopes,
356                config,
357                &go_pkg_index,
358            );
359
360            // Resolve pending call types using the complete return type map
361            inject_return_type_bindings(
362                &entity_inner_scope,
363                &mut scopes,
364                &return_type_map,
365                &local_import_table,
366                file_path,
367                entity_map,
368            );
369
370            let mut file_edges: Vec<(String, String, RefType)> = Vec::new();
371            let mut file_log: Vec<ResolutionEntry> = Vec::new();
372
373            // Walk the AST once for the entire file, collecting all refs with row positions
374            let all_file_refs = collect_all_file_refs(tree.root_node(), source, config);
375
376            for entity in &file_entities {
377                let scope_idx = entity_inner_scope
378                    .get(&entity.id)
379                    .or_else(|| entity_scope_map.get(&entity.id))
380                    .copied()
381                    .unwrap_or(0);
382
383                let start_row = entity.start_line.saturating_sub(1); // 1-indexed to 0-indexed
384                let end_row = entity.end_line; // exclusive
385
386                // Filter pre-collected refs to this entity's line range
387                for ast_ref in all_file_refs.iter().filter(|r| r.row >= start_row && r.row < end_row) {
388                    // Skip self-name refs (was previously done during collection)
389                    let is_self_ref = match &ast_ref.kind {
390                        AstRefKind::Call(name) => name == &entity.name,
391                        AstRefKind::MethodCall { .. } => false,
392                    };
393                    if is_self_ref {
394                        continue;
395                    }
396
397                    let resolution = resolve_ref(
398                        ast_ref,
399                        scope_idx,
400                        &scopes,
401                        &symbol_table,
402                        &class_members,
403                        &local_import_table,
404                        &instance_attr_types,
405                        entity_map,
406                        file_path,
407                        &entity.id,
408                    );
409
410                    if let Some((target_id, ref_type, method)) = resolution {
411                        if target_id != entity.id {
412                            let is_parent_child = entity
413                                .parent_id
414                                .as_ref()
415                                .map_or(false, |pid| pid == &target_id || entity_map.get(&target_id).map_or(false, |t| t.parent_id.as_ref() == Some(&entity.id)));
416
417                            if !is_parent_child {
418                                file_edges.push((
419                                    entity.id.clone(),
420                                    target_id.clone(),
421                                    ref_type,
422                                ));
423                                file_log.push(ResolutionEntry {
424                                    from_entity: entity.id.clone(),
425                                    reference: ref_description(ast_ref),
426                                    resolved_to: Some(target_id),
427                                    method,
428                                });
429                            }
430                        }
431                    } else {
432                        file_log.push(ResolutionEntry {
433                            from_entity: entity.id.clone(),
434                            reference: ref_description(ast_ref),
435                            resolved_to: None,
436                            method: "unresolved",
437                        });
438                    }
439                }
440            }
441
442            Some((file_edges, file_log))
443        })
444        .collect();
445
446    for (file_edges, file_log) in per_file_results {
447        all_edges.extend(file_edges);
448        log.extend(file_log);
449    }
450
451    // Deduplicate edges
452    let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::with_capacity(all_edges.len());
453    let deduped_edges: Vec<(String, String, RefType)> = {
454        let mut result = Vec::with_capacity(all_edges.len());
455        for edge in all_edges {
456            if seen.insert((edge.0.clone(), edge.1.clone())) {
457                result.push(edge);
458            }
459        }
460        result
461    };
462    let all_edges = deduped_edges;
463
464    ScopeResult {
465        edges: all_edges,
466        resolution_log: log,
467    }
468}
469
470fn ref_description(ast_ref: &AstRef) -> String {
471    match &ast_ref.kind {
472        AstRefKind::Call(name) => format!("{}()", name),
473        AstRefKind::MethodCall { receiver, method } => format!("{}.{}()", receiver, method),
474    }
475}
476
477/// Build scope tree by walking the AST.
478/// Creates class scopes and maps methods to them.
479/// Uses an iterative worklist to avoid stack overflow on deeply nested ASTs.
480/// Fixes: https://github.com/Ataraxy-Labs/sem/issues/103
481fn build_scopes_from_ast(
482    root: tree_sitter::Node,
483    root_scope: usize,
484    scopes: &mut Vec<Scope>,
485    entity_scope_map: &mut HashMap<String, usize>,
486    entity_inner_scope: &mut HashMap<String, usize>,
487    file_entities: &[&SemanticEntity],
488    children_by_parent: &HashMap<&str, Vec<&SemanticEntity>>,
489    entity_map: &HashMap<String, EntityInfo>,
490    _file_path: &str,
491    source: &[u8],
492    config: &ScopeResolveConfig,
493) {
494    // Each entry: (node, current_scope)
495    let mut worklist: Vec<(tree_sitter::Node, usize)> = vec![(root, root_scope)];
496
497    while let Some((node, current_scope)) = worklist.pop() {
498        let kind = node.kind();
499
500        // Class-like scope: config-driven
501        let is_class_like = config.class_scope_nodes.contains(&kind);
502
503        // Impl scope: config-driven (Rust impl_item, Swift extension)
504        let is_impl = config.impl_scope_nodes.contains(&kind);
505
506        if is_class_like || is_impl {
507            let class_name = if is_impl {
508                node.child_by_field_name("type")
509                    .and_then(|n| n.utf8_text(source).ok())
510                    .unwrap_or("")
511            } else {
512                match &config.class_name_field {
513                    ClassNameField::Simple(field) => {
514                        node.child_by_field_name(field)
515                            .and_then(|n| n.utf8_text(source).ok())
516                            .unwrap_or("")
517                    }
518                    ClassNameField::TypeSpec { spec_kind, field } => {
519                        let mut name = "";
520                        let mut cursor = node.walk();
521                        for child in node.named_children(&mut cursor) {
522                            if child.kind() == *spec_kind {
523                                name = child
524                                    .child_by_field_name(field)
525                                    .and_then(|n| n.utf8_text(source).ok())
526                                    .unwrap_or("");
527                                break;
528                            }
529                        }
530                        name
531                    }
532                    ClassNameField::ImplType(field) => {
533                        node.child_by_field_name(field)
534                            .and_then(|n| n.utf8_text(source).ok())
535                            .unwrap_or("")
536                    }
537                }
538            };
539
540            let class_entity = file_entities.iter().find(|e| {
541                e.name == class_name
542                    && matches!(e.entity_type.as_str(), "class" | "struct" | "interface")
543            }).copied();
544
545            if let Some(ce) = class_entity {
546                let existing_scope = entity_inner_scope.get(&ce.id).copied();
547
548                let class_scope_idx = if let Some(idx) = existing_scope {
549                    idx
550                } else {
551                    let idx = scopes.len();
552                    scopes.push(Scope {
553                        parent: Some(current_scope),
554                        defs: HashMap::new(),
555                        types: HashMap::new(),
556                        pending_call_types: HashMap::new(),
557                        owner_id: Some(ce.id.clone()),
558                        kind: "class",
559                    });
560                    entity_scope_map.insert(ce.id.clone(), current_scope);
561                    entity_inner_scope.insert(ce.id.clone(), idx);
562                    idx
563                };
564
565                if let Some(children) = children_by_parent.get(ce.id.as_str()) {
566                    for entity in children {
567                        scopes[class_scope_idx]
568                            .defs
569                            .insert(entity.name.clone(), entity.id.clone());
570                        entity_scope_map.insert(entity.id.clone(), class_scope_idx);
571                    }
572                }
573
574                let mut cursor = node.walk();
575                let children: Vec<_> = node.named_children(&mut cursor).collect();
576                for child in children.into_iter().rev() {
577                    worklist.push((child, class_scope_idx));
578                }
579                continue;
580            } else if !is_impl {
581                let class_scope_idx = scopes.len();
582                scopes.push(Scope {
583                    parent: Some(current_scope),
584                    defs: HashMap::new(),
585                    types: HashMap::new(),
586                    pending_call_types: HashMap::new(),
587                    owner_id: None,
588                    kind: "class",
589                });
590                let mut cursor = node.walk();
591                let children: Vec<_> = node.named_children(&mut cursor).collect();
592                for child in children.into_iter().rev() {
593                    worklist.push((child, class_scope_idx));
594                }
595                continue;
596            }
597        }
598
599        // Function-like scope: config-driven
600        let is_function_like = config.function_scope_nodes.contains(&kind);
601
602        if is_function_like {
603            let func_name = node.child_by_field_name("name")
604                .and_then(|n| n.utf8_text(source).ok())
605                .unwrap_or("");
606
607            let parent_scope = if config.external_method && kind == "method_declaration" {
608                let receiver_type = node.utf8_text(source).ok()
609                    .and_then(|t| extract_go_receiver_type(t));
610                if let Some(ref struct_name) = receiver_type {
611                    let found = scopes.iter().enumerate().find(|(_, s)| {
612                        s.kind == "class" && s.owner_id.as_ref().map_or(false, |oid| {
613                            entity_map.get(oid).map_or(false, |e| e.name == *struct_name)
614                        })
615                    });
616                    found.map(|(idx, _)| idx).unwrap_or(current_scope)
617                } else {
618                    current_scope
619                }
620            } else {
621                current_scope
622            };
623
624            let func_scope_idx = scopes.len();
625            scopes.push(Scope {
626                parent: Some(parent_scope),
627                defs: HashMap::new(),
628                types: HashMap::new(),
629                pending_call_types: HashMap::new(),
630                owner_id: None,
631                kind: "function",
632            });
633
634            let func_entity = file_entities.iter().find(|e| {
635                e.name == func_name && {
636                    let line = node.start_position().row + 1;
637                    e.start_line <= line && line <= e.end_line
638                }
639            }).copied();
640
641            if let Some(fe) = func_entity {
642                scopes[func_scope_idx].owner_id = Some(fe.id.clone());
643                entity_scope_map.entry(fe.id.clone()).or_insert(parent_scope);
644                entity_inner_scope.insert(fe.id.clone(), func_scope_idx);
645                if config.external_method && kind == "method_declaration" && parent_scope != current_scope {
646                    scopes[parent_scope].defs.insert(fe.name.clone(), fe.id.clone());
647                }
648            }
649
650            scan_assignments(node, func_scope_idx, scopes, source, config);
651            scan_function_params(node, func_scope_idx, scopes, source, config);
652
653            if config.external_method && kind == "method_declaration" {
654                if let Some(receiver) = node.child_by_field_name("receiver") {
655                    let mut rcursor = receiver.walk();
656                    for param in receiver.named_children(&mut rcursor) {
657                        if param.kind() == "parameter_declaration" {
658                            let param_name = param
659                                .child_by_field_name("name")
660                                .and_then(|n| n.utf8_text(source).ok())
661                                .unwrap_or("");
662                            let param_type = param
663                                .child_by_field_name("type")
664                                .map(|n| extract_base_type(n, source))
665                                .unwrap_or_default();
666                            if !param_name.is_empty() && !param_type.is_empty() {
667                                scopes[func_scope_idx]
668                                    .types
669                                    .insert(param_name.to_string(), param_type);
670                            }
671                        }
672                    }
673                }
674            }
675
676            let mut cursor = node.walk();
677            let children: Vec<_> = node.named_children(&mut cursor).collect();
678            for child in children.into_iter().rev() {
679                worklist.push((child, func_scope_idx));
680            }
681            continue;
682        }
683
684        let mut cursor = node.walk();
685        let children: Vec<_> = node.named_children(&mut cursor).collect();
686        for child in children.into_iter().rev() {
687            worklist.push((child, current_scope));
688        }
689    }
690}
691
692/// Scan for variable assignments and record type bindings.
693fn scan_assignments(
694    root: tree_sitter::Node,
695    scope_idx: usize,
696    scopes: &mut Vec<Scope>,
697    source: &[u8],
698    config: &ScopeResolveConfig,
699) {
700    let mut worklist = vec![root];
701    while let Some(node) = worklist.pop() {
702        let mut cursor = node.walk();
703        for child in node.named_children(&mut cursor) {
704            let ck = child.kind();
705
706            // Check if this node matches an assignment rule
707            for rule in config.assignment_rules {
708                if ck == rule.node_kind {
709                    match rule.strategy {
710                        AssignmentStrategy::LeftRight => {
711                            scan_single_assignment(child, scope_idx, scopes, source);
712                        }
713                        AssignmentStrategy::Declarators => {
714                            scan_ts_var_declaration(child, scope_idx, scopes, source);
715                        }
716                        AssignmentStrategy::PatternBased => {
717                            scan_rust_let_declaration(child, scope_idx, scopes, source);
718                        }
719                        AssignmentStrategy::ShortVar => {
720                            scan_go_short_var(child, scope_idx, scopes, source);
721                        }
722                        AssignmentStrategy::VarSpec => {
723                            scan_go_var_declaration(child, scope_idx, scopes, source);
724                        }
725                    }
726                }
727            }
728
729            // Recurse into configured container nodes
730            if config.assignment_recurse_into.contains(&ck) {
731                worklist.push(child);
732            }
733        }
734    }
735}
736
737/// Scan function parameter type annotations and add them as type bindings.
738/// e.g. `def foo(shelter: Shelter)` -> types["shelter"] = "Shelter"
739fn scan_function_params(
740    node: tree_sitter::Node,
741    scope_idx: usize,
742    scopes: &mut Vec<Scope>,
743    source: &[u8],
744    config: &ScopeResolveConfig,
745) {
746    let params_node = match node.child_by_field_name("parameters") {
747        Some(p) => p,
748        None => return,
749    };
750
751    let mut cursor = params_node.walk();
752    for child in params_node.named_children(&mut cursor) {
753        for rule in config.param_rules {
754            if child.kind() != rule.node_kind {
755                continue;
756            }
757
758            let param_name = match &rule.name_field {
759                ParamNameField::Simple(field) => {
760                    child.child_by_field_name(field)
761                        .and_then(|n| n.utf8_text(source).ok())
762                        .unwrap_or("")
763                }
764                ParamNameField::WithFallback(field) => {
765                    child.child_by_field_name(field)
766                        .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
767                        .and_then(|n| n.utf8_text(source).ok())
768                        .unwrap_or("")
769                }
770                ParamNameField::RustPattern => {
771                    child.child_by_field_name("pattern")
772                        .and_then(|n| {
773                            if n.kind() == "identifier" {
774                                n.utf8_text(source).ok()
775                            } else if n.kind() == "mut_pattern" {
776                                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
777                            } else if n.kind() == "reference_pattern" {
778                                n.named_child(0).and_then(|c| {
779                                    if c.kind() == "identifier" {
780                                        c.utf8_text(source).ok()
781                                    } else if c.kind() == "mut_pattern" {
782                                        c.named_child(0).and_then(|cc| cc.utf8_text(source).ok())
783                                    } else {
784                                        None
785                                    }
786                                })
787                            } else {
788                                None
789                            }
790                        })
791                        .unwrap_or("")
792                }
793            };
794
795            if param_name.is_empty() || rule.skip_names.contains(&param_name) {
796                continue;
797            }
798
799            if let Some(type_node) = child.child_by_field_name(rule.type_field) {
800                let type_text = extract_base_type(type_node, source);
801                if !type_text.is_empty()
802                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
803                {
804                    scopes[scope_idx]
805                        .types
806                        .insert(param_name.to_string(), type_text);
807                }
808            }
809        }
810    }
811}
812
813/// Python/TS: `x = Foo()` or `x = func()`
814fn scan_single_assignment(
815    node: tree_sitter::Node,
816    scope_idx: usize,
817    scopes: &mut Vec<Scope>,
818    source: &[u8],
819) {
820    let assign = if node.kind() == "assignment" {
821        node
822    } else {
823        let mut cursor = node.walk();
824        let children: Vec<_> = node.named_children(&mut cursor).collect();
825        match children.into_iter().find(|c| c.kind() == "assignment" || c.kind() == "assignment_expression") {
826            Some(a) => a,
827            None => return,
828        }
829    };
830
831    let left = match assign.child_by_field_name("left") {
832        Some(l) => l,
833        None => return,
834    };
835    let right = match assign.child_by_field_name("right") {
836        Some(r) => r,
837        None => return,
838    };
839
840    if left.kind() != "identifier" {
841        return;
842    }
843    let var_name = match left.utf8_text(source) {
844        Ok(n) => n.to_string(),
845        Err(_) => return,
846    };
847
848    record_type_from_rhs(right, &var_name, scope_idx, scopes, source);
849}
850
851/// TS: `const x = new Foo()` or `const x: Type = ...` or `const x = func()`
852fn scan_ts_var_declaration(
853    node: tree_sitter::Node,
854    scope_idx: usize,
855    scopes: &mut Vec<Scope>,
856    source: &[u8],
857) {
858    let mut cursor = node.walk();
859    for child in node.named_children(&mut cursor) {
860        if child.kind() == "variable_declarator" {
861            let var_name = child
862                .child_by_field_name("name")
863                .and_then(|n| n.utf8_text(source).ok())
864                .unwrap_or("")
865                .to_string();
866            if var_name.is_empty() {
867                continue;
868            }
869
870            // Check for explicit type annotation: `const x: Foo = ...`
871            if let Some(type_ann) = child.child_by_field_name("type") {
872                let type_text = extract_base_type(type_ann, source);
873                if !type_text.is_empty()
874                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
875                {
876                    scopes[scope_idx]
877                        .types
878                        .insert(var_name.clone(), type_text);
879                    continue;
880                }
881            }
882
883            // Check RHS value
884            if let Some(value) = child.child_by_field_name("value") {
885                record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
886            }
887        }
888    }
889}
890
891/// Rust: `let x: Type = ...` or `let x = Foo::new()`
892fn scan_rust_let_declaration(
893    node: tree_sitter::Node,
894    scope_idx: usize,
895    scopes: &mut Vec<Scope>,
896    source: &[u8],
897) {
898    let var_name = node
899        .child_by_field_name("pattern")
900        .and_then(|n| {
901            // Pattern can be just an identifier or `mut x`
902            if n.kind() == "identifier" {
903                n.utf8_text(source).ok()
904            } else if n.kind() == "mut_pattern" {
905                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
906            } else {
907                None
908            }
909        })
910        .unwrap_or("")
911        .to_string();
912
913    if var_name.is_empty() {
914        return;
915    }
916
917    // Check for explicit type annotation: `let x: Connection = ...`
918    if let Some(type_node) = node.child_by_field_name("type") {
919        let type_text = extract_base_type(type_node, source);
920        if !type_text.is_empty()
921            && type_text.chars().next().map_or(false, |c| c.is_uppercase())
922        {
923            scopes[scope_idx]
924                .types
925                .insert(var_name, type_text);
926            return;
927        }
928    }
929
930    // Check RHS value
931    if let Some(value) = node.child_by_field_name("value") {
932        record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
933    }
934}
935
936/// Go: `x := Foo{}` or `x := NewFoo()`
937fn scan_go_short_var(
938    node: tree_sitter::Node,
939    scope_idx: usize,
940    scopes: &mut Vec<Scope>,
941    source: &[u8],
942) {
943    let left = match node.child_by_field_name("left") {
944        Some(l) => l,
945        None => return,
946    };
947    let right = match node.child_by_field_name("right") {
948        Some(r) => r,
949        None => return,
950    };
951
952    // left is expression_list, right is expression_list
953    let var_name = if left.kind() == "expression_list" {
954        left.named_child(0)
955            .and_then(|n| n.utf8_text(source).ok())
956            .unwrap_or("")
957            .to_string()
958    } else {
959        left.utf8_text(source).unwrap_or("").to_string()
960    };
961
962    if var_name.is_empty() {
963        return;
964    }
965
966    let rhs = if right.kind() == "expression_list" {
967        match right.named_child(0) {
968            Some(n) => n,
969            None => return,
970        }
971    } else {
972        right
973    };
974
975    record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
976}
977
978/// Go: `var x Type = ...` or `var x = Foo{}`
979fn scan_go_var_declaration(
980    node: tree_sitter::Node,
981    scope_idx: usize,
982    scopes: &mut Vec<Scope>,
983    source: &[u8],
984) {
985    let mut cursor = node.walk();
986    for child in node.named_children(&mut cursor) {
987        if child.kind() == "var_spec" {
988            let var_name = child
989                .child_by_field_name("name")
990                .and_then(|n| n.utf8_text(source).ok())
991                .unwrap_or("")
992                .to_string();
993            if var_name.is_empty() {
994                // Try first named child as name
995                if let Some(first) = child.named_child(0) {
996                    if first.kind() == "identifier" {
997                        let name = first.utf8_text(source).unwrap_or("").to_string();
998                        if !name.is_empty() {
999                            // Check for type child
1000                            if let Some(type_node) = child.child_by_field_name("type") {
1001                                let type_text = extract_base_type(type_node, source);
1002                                if !type_text.is_empty()
1003                                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1004                                {
1005                                    scopes[scope_idx].types.insert(name, type_text);
1006                                }
1007                            }
1008                        }
1009                    }
1010                }
1011                continue;
1012            }
1013
1014            // Check for explicit type
1015            if let Some(type_node) = child.child_by_field_name("type") {
1016                let type_text = extract_base_type(type_node, source);
1017                if !type_text.is_empty()
1018                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1019                {
1020                    scopes[scope_idx]
1021                        .types
1022                        .insert(var_name, type_text);
1023                    continue;
1024                }
1025            }
1026
1027            // Check RHS value
1028            if let Some(value) = child.child_by_field_name("value") {
1029                let rhs = if value.kind() == "expression_list" {
1030                    value.named_child(0).unwrap_or(value)
1031                } else {
1032                    value
1033                };
1034                record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
1035            }
1036        }
1037    }
1038}
1039
1040/// Record type binding from a RHS expression (works for all languages).
1041/// Handles: constructor calls, new expressions, struct literals, function calls.
1042fn record_type_from_rhs(
1043    rhs: tree_sitter::Node,
1044    var_name: &str,
1045    scope_idx: usize,
1046    scopes: &mut Vec<Scope>,
1047    source: &[u8],
1048) {
1049    match rhs.kind() {
1050        // Python/Go: Foo() or func()
1051        "call" | "call_expression" => {
1052            let func_node = rhs
1053                .child_by_field_name("function")
1054                .or_else(|| rhs.named_child(0));
1055            if let Some(func) = func_node {
1056                if func.kind() == "identifier" {
1057                    let name = func.utf8_text(source).unwrap_or("");
1058                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1059                        scopes[scope_idx]
1060                            .types
1061                            .insert(var_name.to_string(), name.to_string());
1062                    } else {
1063                        scopes[scope_idx]
1064                            .pending_call_types
1065                            .insert(var_name.to_string(), name.to_string());
1066                    }
1067                }
1068                // Rust: Type::new() / Type::from() etc.
1069                if func.kind() == "scoped_identifier" {
1070                    let text = func.utf8_text(source).unwrap_or("");
1071                    let parts: Vec<&str> = text.split("::").collect();
1072                    if parts.len() >= 2 {
1073                        let type_name = parts[0];
1074                        let method_name = parts[parts.len() - 1];
1075                        if type_name.chars().next().map_or(false, |c| c.is_uppercase()) {
1076                            scopes[scope_idx]
1077                                .types
1078                                .insert(var_name.to_string(), type_name.to_string());
1079                        } else {
1080                            scopes[scope_idx]
1081                                .pending_call_types
1082                                .insert(var_name.to_string(), method_name.to_string());
1083                        }
1084                    }
1085                }
1086                // Go: package.NewFoo() or package.GetFoo()
1087                if func.kind() == "selector_expression" {
1088                    let field = func
1089                        .child_by_field_name("field")
1090                        .and_then(|n| n.utf8_text(source).ok())
1091                        .unwrap_or("");
1092                    // Go convention: NewFoo() returns *Foo
1093                    if let Some(type_name) = field.strip_prefix("New") {
1094                        if !type_name.is_empty()
1095                            && type_name.chars().next().map_or(false, |c| c.is_uppercase())
1096                        {
1097                            scopes[scope_idx]
1098                                .types
1099                                .insert(var_name.to_string(), type_name.to_string());
1100                        }
1101                    } else if field.starts_with("Get") || field.chars().next().map_or(false, |c| c.is_uppercase()) {
1102                        // Other Go package functions: record for return type resolution
1103                        scopes[scope_idx]
1104                            .pending_call_types
1105                            .insert(var_name.to_string(), field.to_string());
1106                    }
1107                }
1108            }
1109        }
1110        // TS: new Foo()
1111        "new_expression" => {
1112            if let Some(constructor) = rhs.child_by_field_name("constructor") {
1113                let name = constructor.utf8_text(source).unwrap_or("");
1114                if !name.is_empty() {
1115                    scopes[scope_idx]
1116                        .types
1117                        .insert(var_name.to_string(), name.to_string());
1118                }
1119            }
1120        }
1121        // Go: Foo{} (composite_literal / struct literal)
1122        "composite_literal" => {
1123            if let Some(type_node) = rhs.child_by_field_name("type") {
1124                let name = type_node.utf8_text(source).unwrap_or("");
1125                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1126                    scopes[scope_idx]
1127                        .types
1128                        .insert(var_name.to_string(), name.to_string());
1129                }
1130            }
1131        }
1132        _ => {}
1133    }
1134}
1135
1136/// Extract the base type name from a type annotation node.
1137/// Strips pointers, references, generics to get just the type name.
1138fn extract_base_type(type_node: tree_sitter::Node, source: &[u8]) -> String {
1139    let text = type_node.utf8_text(source).unwrap_or("").trim().to_string();
1140    // Strip reference/pointer prefixes and mut keyword
1141    let text = text.trim_start_matches('&').trim_start_matches('*');
1142    let text = text.strip_prefix("mut ").unwrap_or(text).trim_start();
1143    // Strip generic parameters (angle brackets and Python-style square brackets)
1144    let text = if let Some(i) = text.find('<') {
1145        &text[..i]
1146    } else if let Some(i) = text.find('[') {
1147        &text[..i]
1148    } else {
1149        text
1150    };
1151    // Strip lifetime annotations for Rust
1152    let text = text.trim();
1153    // For type_annotation nodes in TS, strip the leading `: `
1154    let text = text.trim_start_matches(':').trim();
1155    text.to_string()
1156}
1157
1158/// Parse Go receiver type from method content: `func (r *ReceiverType) Name(...)`
1159pub fn extract_go_receiver_type(content: &str) -> Option<String> {
1160    let after_func = content.strip_prefix("func")?.trim_start();
1161    let paren_start = after_func.find('(')?;
1162    let paren_end = after_func.find(')')?;
1163    let receiver_block = &after_func[paren_start + 1..paren_end];
1164    // Could be: "r ReceiverType", "r *ReceiverType", "*ReceiverType"
1165    let parts: Vec<&str> = receiver_block.split_whitespace().collect();
1166    let type_str = parts.last()?;
1167    let name = type_str.trim_start_matches('*');
1168    if name.is_empty() {
1169        None
1170    } else {
1171        Some(name.to_string())
1172    }
1173}
1174
1175/// Build Go package index: pkg_name → [(entity_name, entity_id)]
1176/// Maps file stems and parent directory names to entities for O(1) package import lookup.
1177fn build_go_pkg_index(
1178    symbol_table: &HashMap<String, Vec<String>>,
1179    entity_map: &HashMap<String, EntityInfo>,
1180) -> HashMap<String, Vec<(String, String)>> {
1181    let mut idx: HashMap<String, Vec<(String, String)>> = HashMap::new();
1182    for (name, target_ids) in symbol_table.iter() {
1183        for target_id in target_ids {
1184            if let Some(entity) = entity_map.get(target_id) {
1185                if !entity.file_path.ends_with(".go") {
1186                    continue;
1187                }
1188                let file_stem = entity.file_path.rsplit('/').next().unwrap_or(&entity.file_path);
1189                let file_stem = file_stem.strip_suffix(".go").unwrap_or(file_stem);
1190                idx.entry(file_stem.to_string())
1191                    .or_default()
1192                    .push((name.clone(), target_id.clone()));
1193                if let Some(parent_start) = entity.file_path.rfind('/') {
1194                    let parent_path = &entity.file_path[..parent_start];
1195                    if let Some(dir_name_start) = parent_path.rfind('/') {
1196                        let dir_name = &parent_path[dir_name_start + 1..];
1197                        if dir_name != file_stem {
1198                            idx.entry(dir_name.to_string())
1199                                .or_default()
1200                                .push((name.clone(), target_id.clone()));
1201                        }
1202                    } else if !parent_path.is_empty() && parent_path != file_stem {
1203                        idx.entry(parent_path.to_string())
1204                            .or_default()
1205                            .push((name.clone(), target_id.clone()));
1206                    }
1207                }
1208            }
1209        }
1210    }
1211    idx
1212}
1213
1214/// Scan function bodies/signatures for return types to build a return type map.
1215fn scan_return_types(
1216    root: tree_sitter::Node,
1217    _file_path: &str,
1218    file_entities: &[&SemanticEntity],
1219    source: &[u8],
1220    return_type_map: &mut HashMap<String, String>,
1221    config: &ScopeResolveConfig,
1222) {
1223    let mut worklist = vec![root];
1224    while let Some(node) = worklist.pop() {
1225        let kind = node.kind();
1226
1227        let is_func = config.function_scope_nodes.contains(&kind);
1228
1229        if is_func {
1230            let func_name = node
1231                .child_by_field_name("name")
1232                .and_then(|n| n.utf8_text(source).ok())
1233                .unwrap_or("");
1234
1235            let func_entity = file_entities.iter().find(|e| {
1236                e.name == func_name && {
1237                    let line = node.start_position().row + 1;
1238                    e.start_line <= line && line <= e.end_line
1239                }
1240            }).copied();
1241
1242            if let Some(fe) = func_entity {
1243                // Try explicit return type annotation first
1244                let ret_type = config.return_type_field.and_then(|field| {
1245                    node.child_by_field_name(field)
1246                        .map(|n| extract_base_type(n, source))
1247                        .filter(|t| !t.is_empty() && t.chars().next().map_or(false, |c| c.is_uppercase()))
1248                });
1249
1250                if let Some(rt) = ret_type {
1251                    return_type_map.insert(fe.id.clone(), rt);
1252                } else {
1253                    // Fall back to body heuristic: return ClassName()
1254                    if let Some(ret_type) = find_return_constructor(node, source) {
1255                        return_type_map.insert(fe.id.clone(), ret_type);
1256                    }
1257                }
1258            }
1259        }
1260
1261        let mut cursor = node.walk();
1262        let children: Vec<_> = node.named_children(&mut cursor).collect();
1263        for child in children.into_iter().rev() {
1264            worklist.push(child);
1265        }
1266    }
1267}
1268
1269/// Find `return ClassName()` patterns in a function body (heuristic fallback).
1270fn find_return_constructor(root: tree_sitter::Node, source: &[u8]) -> Option<String> {
1271    let mut worklist = vec![root];
1272    while let Some(node) = worklist.pop() {
1273        let mut cursor = node.walk();
1274        for child in node.named_children(&mut cursor) {
1275            if child.kind() == "return_statement" {
1276                let mut inner_cursor = child.walk();
1277                for ret_child in child.named_children(&mut inner_cursor) {
1278                    // Python: call, TS/Go: call_expression
1279                    if ret_child.kind() == "call" || ret_child.kind() == "call_expression" {
1280                        if let Some(func) = ret_child.child_by_field_name("function") {
1281                            if func.kind() == "identifier" {
1282                                let name = func.utf8_text(source).unwrap_or("");
1283                                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1284                                    return Some(name.to_string());
1285                                }
1286                            }
1287                        }
1288                    }
1289                    // TS: new ClassName()
1290                    if ret_child.kind() == "new_expression" {
1291                        if let Some(constructor) = ret_child.child_by_field_name("constructor") {
1292                            let name = constructor.utf8_text(source).unwrap_or("");
1293                            if !name.is_empty() {
1294                                return Some(name.to_string());
1295                            }
1296                        }
1297                    }
1298                    // Go: StructName{} (composite_literal)
1299                    if ret_child.kind() == "composite_literal" {
1300                        if let Some(type_node) = ret_child.child_by_field_name("type") {
1301                            let name = type_node.utf8_text(source).unwrap_or("");
1302                            if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1303                                return Some(name.to_string());
1304                            }
1305                        }
1306                    }
1307                }
1308            }
1309            // Recurse into blocks
1310            let ck = child.kind();
1311            if ck == "block" || ck == "statement_block" {
1312                worklist.push(child);
1313            }
1314        }
1315    }
1316    None
1317}
1318
1319/// Scan for instance attribute types: __init__ self.attr patterns (Python/TS),
1320/// struct field declarations (Rust/Go).
1321fn scan_init_self_attrs(
1322    root: tree_sitter::Node,
1323    _file_path: &str,
1324    _file_entities: &[&SemanticEntity],
1325    _entity_map: &HashMap<String, EntityInfo>,
1326    source: &[u8],
1327    instance_attr_types: &mut HashMap<(String, String), String>,
1328    init_params_map: &mut HashMap<String, Vec<String>>,
1329    attr_to_param_map: &mut HashMap<(String, String), String>,
1330    config: &ScopeResolveConfig,
1331) {
1332    let mut worklist = vec![root];
1333    while let Some(node) = worklist.pop() {
1334        let kind = node.kind();
1335
1336        match &config.init_strategy {
1337            InitStrategy::ConstructorBody { class_nodes, self_keyword, .. } => {
1338                if class_nodes.contains(&kind) {
1339                    let class_name = node
1340                        .child_by_field_name("name")
1341                        .and_then(|n| n.utf8_text(source).ok())
1342                        .unwrap_or("")
1343                        .to_string();
1344
1345                    if !class_name.is_empty() {
1346                        // Determine lang for scan_class_for_init (it still needs it for TS field scanning)
1347                        let lang = if *self_keyword == "this" { "typescript" } else { "python" };
1348                        scan_class_for_init(node, &class_name, source, instance_attr_types, init_params_map, attr_to_param_map, lang);
1349                    }
1350                }
1351            }
1352            InitStrategy::StructFields { struct_nodes } => {
1353                if struct_nodes.contains(&kind) {
1354                    // Rust struct: extract field types directly
1355                    if kind == "struct_item" {
1356                        let struct_name = node
1357                            .child_by_field_name("name")
1358                            .and_then(|n| n.utf8_text(source).ok())
1359                            .unwrap_or("")
1360                            .to_string();
1361
1362                        if !struct_name.is_empty() {
1363                            scan_rust_struct_fields(node, &struct_name, source, instance_attr_types);
1364                        }
1365                    }
1366                    // Go: extract field types from type declarations
1367                    if kind == "type_declaration" {
1368                        scan_go_struct_fields(node, source, instance_attr_types);
1369                    }
1370                }
1371            }
1372            InitStrategy::None => {}
1373        }
1374
1375        let mut cursor = node.walk();
1376        let children: Vec<_> = node.named_children(&mut cursor).collect();
1377        for child in children.into_iter().rev() {
1378            worklist.push(child);
1379        }
1380    }
1381}
1382
1383/// Rust: extract field types from `struct Foo { conn: Connection, ... }`
1384fn scan_rust_struct_fields(
1385    node: tree_sitter::Node,
1386    struct_name: &str,
1387    source: &[u8],
1388    instance_attr_types: &mut HashMap<(String, String), String>,
1389) {
1390    let mut cursor = node.walk();
1391    for child in node.named_children(&mut cursor) {
1392        if child.kind() == "field_declaration_list" {
1393            let mut inner_cursor = child.walk();
1394            for field in child.named_children(&mut inner_cursor) {
1395                if field.kind() == "field_declaration" {
1396                    let field_name = field
1397                        .child_by_field_name("name")
1398                        .and_then(|n| n.utf8_text(source).ok())
1399                        .unwrap_or("");
1400                    let field_type = field
1401                        .child_by_field_name("type")
1402                        .map(|n| extract_base_type(n, source))
1403                        .unwrap_or_default();
1404
1405                    if !field_name.is_empty()
1406                        && !field_type.is_empty()
1407                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1408                    {
1409                        instance_attr_types.insert(
1410                            (struct_name.to_string(), field_name.to_string()),
1411                            field_type,
1412                        );
1413                    }
1414                }
1415            }
1416        }
1417    }
1418}
1419
1420/// Go: extract field types from `type Foo struct { conn Connection; ... }`
1421fn scan_go_struct_fields(
1422    node: tree_sitter::Node,
1423    source: &[u8],
1424    instance_attr_types: &mut HashMap<(String, String), String>,
1425) {
1426    let mut cursor = node.walk();
1427    for child in node.named_children(&mut cursor) {
1428        if child.kind() == "type_spec" {
1429            let struct_name = child
1430                .child_by_field_name("name")
1431                .and_then(|n| n.utf8_text(source).ok())
1432                .unwrap_or("")
1433                .to_string();
1434
1435            if struct_name.is_empty() {
1436                continue;
1437            }
1438
1439            // Look for struct_type child
1440            if let Some(type_node) = child.child_by_field_name("type") {
1441                if type_node.kind() == "struct_type" {
1442                    let mut fields_cursor = type_node.walk();
1443                    for field_list in type_node.named_children(&mut fields_cursor) {
1444                        if field_list.kind() == "field_declaration_list" {
1445                            let mut inner = field_list.walk();
1446                            for field in field_list.named_children(&mut inner) {
1447                                if field.kind() == "field_declaration" {
1448                                    // Go field: name type
1449                                    let field_name = field
1450                                        .child_by_field_name("name")
1451                                        .and_then(|n| n.utf8_text(source).ok())
1452                                        .unwrap_or("");
1453                                    let field_type = field
1454                                        .child_by_field_name("type")
1455                                        .map(|n| extract_base_type(n, source))
1456                                        .unwrap_or_default();
1457
1458                                    if !field_name.is_empty()
1459                                        && !field_type.is_empty()
1460                                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1461                                    {
1462                                        instance_attr_types.insert(
1463                                            (struct_name.clone(), field_name.to_string()),
1464                                            field_type,
1465                                        );
1466                                    }
1467                                }
1468                            }
1469                        }
1470                    }
1471                }
1472            }
1473        }
1474    }
1475}
1476
1477fn scan_class_for_init(
1478    root: tree_sitter::Node,
1479    class_name: &str,
1480    source: &[u8],
1481    instance_attr_types: &mut HashMap<(String, String), String>,
1482    init_params_map: &mut HashMap<String, Vec<String>>,
1483    attr_to_param_map: &mut HashMap<(String, String), String>,
1484    lang: &str,
1485) {
1486    let mut worklist = vec![root];
1487    while let Some(node) = worklist.pop() {
1488        let mut cursor = node.walk();
1489        for child in node.named_children(&mut cursor) {
1490            let ck = child.kind();
1491
1492            // Python __init__
1493            if ck == "function_definition" {
1494                let name = child
1495                    .child_by_field_name("name")
1496                    .and_then(|n| n.utf8_text(source).ok())
1497                    .unwrap_or("");
1498                if name == "__init__" {
1499                    let params = extract_init_params(child, source);
1500                    let ordered_params = extract_init_param_names_ordered(child, source);
1501                    init_params_map.insert(class_name.to_string(), ordered_params);
1502                    scan_init_body(child, class_name, &params, source, instance_attr_types, attr_to_param_map);
1503                }
1504            }
1505
1506            // TS constructor
1507            if ck == "method_definition" && lang == "typescript" {
1508                let name = child
1509                    .child_by_field_name("name")
1510                    .and_then(|n| n.utf8_text(source).ok())
1511                    .unwrap_or("");
1512                if name == "constructor" {
1513                    // Scan for this.attr = param patterns
1514                    scan_ts_constructor_body(child, class_name, source, instance_attr_types, init_params_map, attr_to_param_map);
1515                }
1516            }
1517
1518            // TS: typed class field declarations `private conn: Connection`
1519            if (ck == "public_field_definition" || ck == "property_declaration" || ck == "field_definition") && lang == "typescript" {
1520                let field_name = child
1521                    .child_by_field_name("name")
1522                    .and_then(|n| n.utf8_text(source).ok())
1523                    .unwrap_or("");
1524                if let Some(type_ann) = child.child_by_field_name("type") {
1525                    let type_text = extract_base_type(type_ann, source);
1526                    if !field_name.is_empty()
1527                        && !type_text.is_empty()
1528                        && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1529                    {
1530                        instance_attr_types.insert(
1531                            (class_name.to_string(), field_name.to_string()),
1532                            type_text,
1533                        );
1534                    }
1535                }
1536            }
1537
1538            if ck == "block" || ck == "class_body" || ck == "statement_block" {
1539                worklist.push(child);
1540            }
1541        }
1542    }
1543}
1544
1545/// TS: scan constructor body for `this.attr = param` patterns
1546fn scan_ts_constructor_body(
1547    node: tree_sitter::Node,
1548    class_name: &str,
1549    source: &[u8],
1550    instance_attr_types: &mut HashMap<(String, String), String>,
1551    init_params_map: &mut HashMap<String, Vec<String>>,
1552    attr_to_param_map: &mut HashMap<(String, String), String>,
1553) {
1554    // Extract constructor params
1555    let params = extract_init_params(node, source);
1556    let ordered_params = extract_init_param_names_ordered(node, source);
1557    init_params_map.insert(class_name.to_string(), ordered_params);
1558
1559    // Scan body for this.X = param
1560    scan_init_body_this(node, class_name, &params, source, instance_attr_types, attr_to_param_map);
1561}
1562
1563/// Scan constructor body for `this.attr = param` patterns (TS variant)
1564fn scan_init_body_this(
1565    root: tree_sitter::Node,
1566    class_name: &str,
1567    params: &HashMap<String, Option<String>>,
1568    source: &[u8],
1569    instance_attr_types: &mut HashMap<(String, String), String>,
1570    attr_to_param_map: &mut HashMap<(String, String), String>,
1571) {
1572    let mut worklist = vec![root];
1573    while let Some(node) = worklist.pop() {
1574        let mut cursor = node.walk();
1575        for child in node.named_children(&mut cursor) {
1576            let ck = child.kind();
1577            if ck == "expression_statement" {
1578                // Look for assignment: this.X = Y
1579                let mut inner_cursor = child.walk();
1580                for inner in child.named_children(&mut inner_cursor) {
1581                    if inner.kind() == "assignment_expression" {
1582                        if let Some(left) = inner.child_by_field_name("left") {
1583                            if left.kind() == "member_expression" {
1584                                let obj = left.child_by_field_name("object")
1585                                    .and_then(|n| n.utf8_text(source).ok())
1586                                    .unwrap_or("");
1587                                let prop = left.child_by_field_name("property")
1588                                    .and_then(|n| n.utf8_text(source).ok())
1589                                    .unwrap_or("");
1590                                if obj == "this" && !prop.is_empty() {
1591                                    if let Some(right) = inner.child_by_field_name("right") {
1592                                        if right.kind() == "identifier" {
1593                                            let rhs_name = right.utf8_text(source).unwrap_or("");
1594                                            if params.contains_key(rhs_name) {
1595                                                attr_to_param_map.insert(
1596                                                    (class_name.to_string(), prop.to_string()),
1597                                                    rhs_name.to_string(),
1598                                                );
1599                                                if let Some(Some(type_hint)) = params.get(rhs_name) {
1600                                                    instance_attr_types.insert(
1601                                                        (class_name.to_string(), prop.to_string()),
1602                                                        type_hint.clone(),
1603                                                    );
1604                                                }
1605                                            }
1606                                        }
1607                                        if right.kind() == "new_expression" {
1608                                            if let Some(ctor) = right.child_by_field_name("constructor") {
1609                                                let name = ctor.utf8_text(source).unwrap_or("");
1610                                                if !name.is_empty() {
1611                                                    instance_attr_types.insert(
1612                                                        (class_name.to_string(), prop.to_string()),
1613                                                        name.to_string(),
1614                                                    );
1615                                                }
1616                                            }
1617                                        }
1618                                    }
1619                                }
1620                            }
1621                        }
1622                    }
1623                }
1624            }
1625            if ck == "statement_block" || ck == "block" {
1626                worklist.push(child);
1627            }
1628        }
1629    }
1630}
1631
1632/// Extract __init__ parameter names in order (excluding self).
1633fn extract_init_param_names_ordered(func_node: tree_sitter::Node, source: &[u8]) -> Vec<String> {
1634    let mut names = Vec::new();
1635    if let Some(params_node) = func_node.child_by_field_name("parameters") {
1636        let mut cursor = params_node.walk();
1637        for child in params_node.named_children(&mut cursor) {
1638            let param_name = if child.kind() == "identifier" {
1639                child.utf8_text(source).unwrap_or("").to_string()
1640            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
1641                child.child_by_field_name("name")
1642                    .or_else(|| child.named_child(0))
1643                    .and_then(|n| n.utf8_text(source).ok())
1644                    .unwrap_or("")
1645                    .to_string()
1646            } else {
1647                continue;
1648            };
1649            if param_name != "self" && param_name != "cls" && !param_name.is_empty() {
1650                names.push(param_name);
1651            }
1652        }
1653    }
1654    names
1655}
1656
1657fn extract_init_params(func_node: tree_sitter::Node, source: &[u8]) -> HashMap<String, Option<String>> {
1658    let mut params = HashMap::new();
1659    if let Some(params_node) = func_node.child_by_field_name("parameters") {
1660        let mut cursor = params_node.walk();
1661        for child in params_node.named_children(&mut cursor) {
1662            let param_name = if child.kind() == "identifier" {
1663                child.utf8_text(source).unwrap_or("").to_string()
1664            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
1665                child.child_by_field_name("name")
1666                    .or_else(|| child.named_child(0))
1667                    .and_then(|n| n.utf8_text(source).ok())
1668                    .unwrap_or("")
1669                    .to_string()
1670            } else {
1671                continue;
1672            };
1673            if param_name != "self" && param_name != "cls" {
1674                // Check for type annotation
1675                let type_hint = child.child_by_field_name("type")
1676                    .and_then(|n| n.utf8_text(source).ok())
1677                    .map(|s| s.to_string());
1678                params.insert(param_name, type_hint);
1679            }
1680        }
1681    }
1682    params
1683}
1684
1685fn scan_init_body(
1686    root: tree_sitter::Node,
1687    class_name: &str,
1688    params: &HashMap<String, Option<String>>,
1689    source: &[u8],
1690    instance_attr_types: &mut HashMap<(String, String), String>,
1691    attr_to_param_map: &mut HashMap<(String, String), String>,
1692) {
1693    let mut worklist = vec![root];
1694    while let Some(node) = worklist.pop() {
1695        let mut cursor = node.walk();
1696        for child in node.named_children(&mut cursor) {
1697            if child.kind() == "expression_statement" || child.kind() == "assignment" {
1698                let assign = if child.kind() == "assignment" {
1699                    child
1700                } else {
1701                    let mut inner_cursor = child.walk();
1702                    let children: Vec<_> = child.named_children(&mut inner_cursor).collect();
1703                    match children.into_iter().find(|c| c.kind() == "assignment") {
1704                        Some(a) => a,
1705                        None => continue,
1706                    }
1707                };
1708
1709                if let Some(left) = assign.child_by_field_name("left") {
1710                    if left.kind() == "attribute" {
1711                        let obj = left.child_by_field_name("object")
1712                            .and_then(|n| n.utf8_text(source).ok())
1713                            .unwrap_or("");
1714                        let attr = left.child_by_field_name("attribute")
1715                            .and_then(|n| n.utf8_text(source).ok())
1716                            .unwrap_or("");
1717
1718                        if obj == "self" && !attr.is_empty() {
1719                            if let Some(right) = assign.child_by_field_name("right") {
1720                                if right.kind() == "identifier" {
1721                                    let rhs_name = right.utf8_text(source).unwrap_or("");
1722                                    // Record attr -> param mapping for later inference
1723                                    if params.contains_key(rhs_name) {
1724                                        attr_to_param_map.insert(
1725                                            (class_name.to_string(), attr.to_string()),
1726                                            rhs_name.to_string(),
1727                                        );
1728                                    }
1729                                    // If param has type hint, directly set the type
1730                                    if let Some(Some(type_hint)) = params.get(rhs_name) {
1731                                        instance_attr_types.insert(
1732                                            (class_name.to_string(), attr.to_string()),
1733                                            type_hint.clone(),
1734                                        );
1735                                    }
1736                                }
1737                                if right.kind() == "call" {
1738                                    if let Some(func) = right.child_by_field_name("function") {
1739                                        if func.kind() == "identifier" {
1740                                            let fname = func.utf8_text(source).unwrap_or("");
1741                                            if fname.chars().next().map_or(false, |c| c.is_uppercase()) {
1742                                                instance_attr_types.insert(
1743                                                    (class_name.to_string(), attr.to_string()),
1744                                                    fname.to_string(),
1745                                                );
1746                                            }
1747                                        }
1748                                    }
1749                                }
1750                            }
1751                        }
1752                    }
1753                }
1754            }
1755            if child.kind() == "block" {
1756                worklist.push(child);
1757            }
1758        }
1759    }
1760}
1761
1762/// Infer constructor parameter types by analyzing call sites across all files.
1763/// For `Transaction(get_connection())`, we know get_connection() returns Connection,
1764/// so Transaction.__init__'s conn param has type Connection,
1765/// and self.conn in Transaction has type Connection.
1766fn infer_constructor_param_types(
1767    parsed_files: &[(String, String, tree_sitter::Tree)],
1768    return_type_map: &HashMap<String, String>,
1769    init_params: &HashMap<String, Vec<String>>,
1770    attr_to_param: &HashMap<(String, String), String>,
1771    _symbol_table: &HashMap<String, Vec<String>>,
1772    entity_map: &HashMap<String, EntityInfo>,
1773    instance_attr_types: &mut HashMap<(String, String), String>,
1774) {
1775    // Build func_name -> return_type lookup for quick access
1776    let mut func_name_returns: HashMap<String, String> = HashMap::new();
1777    for (eid, ret_type) in return_type_map {
1778        if let Some(info) = entity_map.get(eid) {
1779            func_name_returns.insert(info.name.clone(), ret_type.clone());
1780        }
1781    }
1782
1783    // Scan all files for constructor call sites: ClassName(arg1, arg2, ...)
1784    // Parallelized: each file produces local results, then merged.
1785    let local_results: Vec<HashMap<(String, String), String>> = parsed_files
1786        .par_iter()
1787        .map(|(_file_path, content, tree)| {
1788            let source = content.as_bytes();
1789            let mut local_attr_types: HashMap<(String, String), String> = HashMap::new();
1790            scan_constructor_calls(
1791                tree.root_node(),
1792                source,
1793                &func_name_returns,
1794                init_params,
1795                attr_to_param,
1796                &mut local_attr_types,
1797            );
1798            local_attr_types
1799        })
1800        .collect();
1801
1802    for local in local_results {
1803        for (key, val) in local {
1804            instance_attr_types.entry(key).or_insert(val);
1805        }
1806    }
1807}
1808
1809fn scan_constructor_calls(
1810    root: tree_sitter::Node,
1811    source: &[u8],
1812    func_name_returns: &HashMap<String, String>,
1813    init_params: &HashMap<String, Vec<String>>,
1814    attr_to_param: &HashMap<(String, String), String>,
1815    instance_attr_types: &mut HashMap<(String, String), String>,
1816) {
1817    let mut worklist = vec![root];
1818    while let Some(node) = worklist.pop() {
1819        let kind = node.kind();
1820
1821        if kind == "call" {
1822            if let Some(func) = node.child_by_field_name("function") {
1823                if func.kind() == "identifier" {
1824                    let class_name = func.utf8_text(source).unwrap_or("");
1825                    // Only process uppercase names (constructor calls)
1826                    if class_name.chars().next().map_or(false, |c| c.is_uppercase()) {
1827                        if let Some(param_names) = init_params.get(class_name) {
1828                            // Extract argument types
1829                            if let Some(args_node) = node.child_by_field_name("arguments") {
1830                                let mut arg_idx = 0;
1831                                let mut args_cursor = args_node.walk();
1832                                for arg in args_node.named_children(&mut args_cursor) {
1833                                    if arg_idx >= param_names.len() {
1834                                        break;
1835                                    }
1836                                    let param_name = &param_names[arg_idx];
1837
1838                                    // Try to infer the argument's type
1839                                    let arg_type = infer_expr_type(arg, source, func_name_returns);
1840
1841                                    if let Some(at) = arg_type {
1842                                        // Check if any self.attr maps to this param
1843                                        for ((cn, attr), pn) in attr_to_param.iter() {
1844                                            if cn == class_name && pn == param_name {
1845                                                instance_attr_types
1846                                                    .entry((cn.clone(), attr.clone()))
1847                                                    .or_insert(at.clone());
1848                                            }
1849                                        }
1850                                    }
1851
1852                                    arg_idx += 1;
1853                                }
1854                            }
1855                        }
1856                    }
1857                }
1858            }
1859        }
1860
1861        let mut cursor = node.walk();
1862        let children: Vec<_> = node.named_children(&mut cursor).collect();
1863        for child in children.into_iter().rev() {
1864            worklist.push(child);
1865        }
1866    }
1867}
1868
1869/// Infer the type of an expression node.
1870fn infer_expr_type(
1871    node: tree_sitter::Node,
1872    source: &[u8],
1873    func_name_returns: &HashMap<String, String>,
1874) -> Option<String> {
1875    match node.kind() {
1876        "call" => {
1877            if let Some(func) = node.child_by_field_name("function") {
1878                if func.kind() == "identifier" {
1879                    let name = func.utf8_text(source).unwrap_or("");
1880                    // Constructor call: Foo() -> type is Foo
1881                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1882                        return Some(name.to_string());
1883                    }
1884                    // Function call with known return type
1885                    if let Some(ret) = func_name_returns.get(name) {
1886                        return Some(ret.clone());
1887                    }
1888                }
1889            }
1890            None
1891        }
1892        "identifier" => {
1893            // Could be a variable, but we don't have scope info here
1894            None
1895        }
1896        _ => None,
1897    }
1898}
1899
1900/// Resolve pending call types using the return type map.
1901/// For scopes with `x = func()` where func has a known return type, bind x to that type.
1902fn inject_return_type_bindings(
1903    _entity_inner_scope: &HashMap<String, usize>,
1904    scopes: &mut Vec<Scope>,
1905    return_type_map: &HashMap<String, String>,
1906    import_table: &HashMap<(String, String), String>,
1907    file_path: &str,
1908    entity_map: &HashMap<String, EntityInfo>,
1909) {
1910    // Build function name -> return type lookup
1911    let mut func_name_return_types: HashMap<String, String> = HashMap::new();
1912    for (eid, ret_type) in return_type_map {
1913        if let Some(info) = entity_map.get(eid) {
1914            func_name_return_types.insert(info.name.clone(), ret_type.clone());
1915        }
1916    }
1917
1918    // Also resolve through imports: if `get_connection` is imported and has a known return type
1919    for ((fp, local_name), target_id) in import_table {
1920        if fp == file_path {
1921            if let Some(ret_type) = return_type_map.get(target_id) {
1922                func_name_return_types.insert(local_name.clone(), ret_type.clone());
1923            }
1924        }
1925    }
1926
1927    // Resolve pending call types in all scopes
1928    for scope in scopes.iter_mut() {
1929        let resolved: Vec<(String, String)> = scope
1930            .pending_call_types
1931            .iter()
1932            .filter_map(|(var_name, func_name)| {
1933                func_name_return_types
1934                    .get(func_name)
1935                    .map(|ret_type| (var_name.clone(), ret_type.clone()))
1936            })
1937            .collect();
1938
1939        for (var_name, ret_type) in resolved {
1940            scope.types.insert(var_name, ret_type);
1941        }
1942    }
1943}
1944
1945/// Extract import statements from the AST.
1946fn extract_imports_from_ast(
1947    root: tree_sitter::Node,
1948    file_path: &str,
1949    source: &[u8],
1950    symbol_table: &HashMap<String, Vec<String>>,
1951    entity_map: &HashMap<String, EntityInfo>,
1952    import_table: &mut HashMap<(String, String), String>,
1953    scopes: &mut Vec<Scope>,
1954    config: &ScopeResolveConfig,
1955    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
1956) {
1957    let mut worklist = vec![root];
1958    while let Some(node) = worklist.pop() {
1959        let mut cursor = node.walk();
1960        for child in node.named_children(&mut cursor) {
1961            let ck = child.kind();
1962            let handled = match ck {
1963                "import_from_statement" => {
1964                    extract_python_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
1965                    true
1966                }
1967                "import_statement" if !config.self_keywords.contains(&"cls") => {
1968                    // TS import_statement (not Python - Python uses import_from_statement)
1969                    extract_ts_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
1970                    true
1971                }
1972                "use_declaration" => {
1973                    extract_rust_use(child, file_path, source, symbol_table, entity_map, import_table, scopes);
1974                    true
1975                }
1976                "import_declaration" => {
1977                    extract_go_import(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
1978                    true
1979                }
1980                _ => false,
1981            };
1982            if !handled {
1983                worklist.push(child);
1984            }
1985        }
1986    }
1987}
1988
1989/// TS: `import { Foo, Bar } from './module'` or `import Foo from './module'`
1990fn extract_ts_import(
1991    node: tree_sitter::Node,
1992    file_path: &str,
1993    source: &[u8],
1994    symbol_table: &HashMap<String, Vec<String>>,
1995    entity_map: &HashMap<String, EntityInfo>,
1996    import_table: &mut HashMap<(String, String), String>,
1997    scopes: &mut Vec<Scope>,
1998) {
1999    // Extract the source module from the `from '...'` clause
2000    let source_path = node
2001        .child_by_field_name("source")
2002        .and_then(|n| n.utf8_text(source).ok())
2003        .unwrap_or("")
2004        .trim_matches(|c: char| c == '\'' || c == '"');
2005
2006    let source_module = source_path
2007        .rsplit('/')
2008        .next()
2009        .unwrap_or(source_path);
2010    // Strip extensions
2011    let source_module = source_module
2012        .strip_suffix(".ts").or_else(|| source_module.strip_suffix(".js"))
2013        .or_else(|| source_module.strip_suffix(".tsx")).or_else(|| source_module.strip_suffix(".jsx"))
2014        .unwrap_or(source_module);
2015
2016    if source_module.is_empty() {
2017        return;
2018    }
2019
2020    // Walk children to find import clause
2021    let mut cursor = node.walk();
2022    for child in node.named_children(&mut cursor) {
2023        if child.kind() == "import_clause" {
2024            let mut clause_cursor = child.walk();
2025            for clause_child in child.named_children(&mut clause_cursor) {
2026                if clause_child.kind() == "named_imports" {
2027                    // { Foo, Bar as Baz }
2028                    let mut imports_cursor = clause_child.walk();
2029                    for spec in clause_child.named_children(&mut imports_cursor) {
2030                        if spec.kind() == "import_specifier" {
2031                            let original = spec
2032                                .child_by_field_name("name")
2033                                .and_then(|n| n.utf8_text(source).ok())
2034                                .unwrap_or("");
2035                            let local = spec
2036                                .child_by_field_name("alias")
2037                                .and_then(|n| n.utf8_text(source).ok())
2038                                .unwrap_or(original);
2039
2040                            if !original.is_empty() {
2041                                resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2042                            }
2043                        }
2044                    }
2045                } else if clause_child.kind() == "identifier" {
2046                    // Default import: import Foo from './module'
2047                    let name = clause_child.utf8_text(source).unwrap_or("");
2048                    if !name.is_empty() {
2049                        resolve_import_name(name, name, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2050                    }
2051                }
2052            }
2053        }
2054    }
2055}
2056
2057/// Rust: `use crate::module::Name;` or `use crate::module::{A, B};`
2058/// Parse from the text of the use_declaration for reliability.
2059fn extract_rust_use(
2060    node: tree_sitter::Node,
2061    file_path: &str,
2062    source: &[u8],
2063    symbol_table: &HashMap<String, Vec<String>>,
2064    entity_map: &HashMap<String, EntityInfo>,
2065    import_table: &mut HashMap<(String, String), String>,
2066    scopes: &mut Vec<Scope>,
2067) {
2068    let text = node.utf8_text(source).unwrap_or("").trim().to_string();
2069    // Strip `use ` prefix and trailing `;`
2070    let text = text.strip_prefix("use ").unwrap_or(&text);
2071    let text = text.strip_prefix("pub use ").unwrap_or(text);
2072    let text = text.trim_end_matches(';').trim();
2073
2074    // Strip crate/super/self prefix
2075    let text = text
2076        .strip_prefix("crate::")
2077        .or_else(|| text.strip_prefix("super::"))
2078        .or_else(|| text.strip_prefix("self::"))
2079        .unwrap_or(text);
2080
2081    // Check for grouped import: module::{A, B, C}
2082    if let Some(brace_pos) = text.find("::{") {
2083        let module_path = &text[..brace_pos];
2084        let source_module = module_path.rsplit("::").next().unwrap_or(module_path);
2085
2086        let names_part = &text[brace_pos + 3..];
2087        let names_part = names_part.trim_end_matches('}');
2088
2089        for name_part in names_part.split(',') {
2090            let name_part = name_part.trim();
2091            if name_part.is_empty() {
2092                continue;
2093            }
2094            let (original, local) = if let Some(pos) = name_part.find(" as ") {
2095                (name_part[..pos].trim(), name_part[pos + 4..].trim())
2096            } else {
2097                (name_part, name_part)
2098            };
2099            if !original.is_empty() {
2100                resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2101            }
2102        }
2103    } else {
2104        // Simple import: module::Name
2105        let parts: Vec<&str> = text.split("::").collect();
2106        if parts.is_empty() {
2107            return;
2108        }
2109        let imported_name = parts.last().unwrap().trim();
2110        let (original, local) = if let Some(pos) = imported_name.find(" as ") {
2111            (&imported_name[..pos], imported_name[pos + 4..].trim())
2112        } else {
2113            (imported_name, imported_name)
2114        };
2115        let source_module = if parts.len() >= 2 {
2116            parts[parts.len() - 2]
2117        } else {
2118            parts[0]
2119        };
2120        if !original.is_empty() && !source_module.is_empty() {
2121            resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2122        }
2123    }
2124}
2125
2126/// Go: `import ("module/path")` - maps package names to entities
2127fn extract_go_import(
2128    node: tree_sitter::Node,
2129    file_path: &str,
2130    source: &[u8],
2131    symbol_table: &HashMap<String, Vec<String>>,
2132    entity_map: &HashMap<String, EntityInfo>,
2133    import_table: &mut HashMap<(String, String), String>,
2134    scopes: &mut Vec<Scope>,
2135    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2136) {
2137    let mut cursor = node.walk();
2138    for child in node.named_children(&mut cursor) {
2139        if child.kind() == "import_spec" || child.kind() == "import_spec_list" {
2140            extract_go_import_specs(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2141        } else if child.kind() == "interpreted_string_literal" || child.kind() == "raw_string_literal" {
2142            let path = child.utf8_text(source).unwrap_or("")
2143                .trim_matches('"').trim_matches('`');
2144            let pkg_name = path.rsplit('/').next().unwrap_or(path);
2145            register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2146        }
2147    }
2148}
2149
2150fn extract_go_import_specs(
2151    root: tree_sitter::Node,
2152    file_path: &str,
2153    source: &[u8],
2154    symbol_table: &HashMap<String, Vec<String>>,
2155    entity_map: &HashMap<String, EntityInfo>,
2156    import_table: &mut HashMap<(String, String), String>,
2157    scopes: &mut Vec<Scope>,
2158    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2159) {
2160    let mut worklist = vec![root];
2161    while let Some(node) = worklist.pop() {
2162        let mut cursor = node.walk();
2163        for child in node.named_children(&mut cursor) {
2164            if child.kind() == "import_spec" {
2165                let path_node = child.child_by_field_name("path")
2166                    .or_else(|| child.named_child(0));
2167                if let Some(pn) = path_node {
2168                    let path = pn.utf8_text(source).unwrap_or("")
2169                        .trim_matches('"').trim_matches('`');
2170                    let pkg_name = path.rsplit('/').next().unwrap_or(path);
2171                    register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2172                }
2173            } else {
2174                worklist.push(child);
2175            }
2176        }
2177    }
2178}
2179
2180fn register_go_package_imports(
2181    pkg_name: &str,
2182    file_path: &str,
2183    _symbol_table: &HashMap<String, Vec<String>>,
2184    _entity_map: &HashMap<String, EntityInfo>,
2185    import_table: &mut HashMap<(String, String), String>,
2186    scopes: &mut Vec<Scope>,
2187    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2188) {
2189    // Use pre-built package index for O(1) lookup instead of O(symbol_table) scan
2190    if let Some(entries) = go_pkg_index.get(pkg_name) {
2191        for (name, target_id) in entries {
2192            import_table.insert(
2193                (file_path.to_string(), name.clone()),
2194                target_id.clone(),
2195            );
2196            if !scopes.is_empty() {
2197                scopes[0].defs.insert(name.clone(), target_id.clone());
2198            }
2199        }
2200    }
2201}
2202
2203/// Shared helper: resolve an imported name against the symbol table
2204fn resolve_import_name(
2205    original_name: &str,
2206    local_name: &str,
2207    source_module: &str,
2208    file_path: &str,
2209    symbol_table: &HashMap<String, Vec<String>>,
2210    entity_map: &HashMap<String, EntityInfo>,
2211    import_table: &mut HashMap<(String, String), String>,
2212    scopes: &mut Vec<Scope>,
2213) {
2214    if let Some(target_ids) = symbol_table.get(original_name) {
2215        let target = target_ids.iter().find(|id| {
2216            entity_map.get(*id).map_or(false, |e| {
2217                let stem = e.file_path.rsplit('/').next().unwrap_or(&e.file_path);
2218                let stem = stem
2219                    .strip_suffix(".py")
2220                    .or_else(|| stem.strip_suffix(".rs"))
2221                    .or_else(|| stem.strip_suffix(".ts"))
2222                    .or_else(|| stem.strip_suffix(".tsx"))
2223                    .or_else(|| stem.strip_suffix(".js"))
2224                    .or_else(|| stem.strip_suffix(".jsx"))
2225                    .or_else(|| stem.strip_suffix(".go"))
2226                    .unwrap_or(stem);
2227                stem == source_module
2228            })
2229        });
2230
2231        if let Some(target_id) = target {
2232            import_table.insert(
2233                (file_path.to_string(), local_name.to_string()),
2234                target_id.clone(),
2235            );
2236            if !scopes.is_empty() {
2237                scopes[0]
2238                    .defs
2239                    .insert(local_name.to_string(), target_id.clone());
2240            }
2241        }
2242    }
2243}
2244
2245fn extract_python_import(
2246    node: tree_sitter::Node,
2247    file_path: &str,
2248    source: &[u8],
2249    symbol_table: &HashMap<String, Vec<String>>,
2250    entity_map: &HashMap<String, EntityInfo>,
2251    import_table: &mut HashMap<(String, String), String>,
2252    scopes: &mut Vec<Scope>,
2253) {
2254    // import_from_statement has:
2255    //   module_name (dotted_name or relative_import)
2256    //   name fields (imported names)
2257    let module_node = node.child_by_field_name("module_name");
2258    let module_name = module_node
2259        .and_then(|n| n.utf8_text(source).ok())
2260        .unwrap_or("");
2261
2262    let source_module = module_name
2263        .trim_start_matches('.')
2264        .rsplit('.')
2265        .next()
2266        .unwrap_or(module_name.trim_start_matches('.'));
2267
2268    // Walk children to find imported names
2269    let mut cursor = node.walk();
2270    for child in node.named_children(&mut cursor) {
2271        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
2272            let (original, local) = if child.kind() == "aliased_import" {
2273                let orig = child
2274                    .child_by_field_name("name")
2275                    .and_then(|n| n.utf8_text(source).ok())
2276                    .unwrap_or("");
2277                let alias = child
2278                    .child_by_field_name("alias")
2279                    .and_then(|n| n.utf8_text(source).ok())
2280                    .unwrap_or(orig);
2281                (orig, alias)
2282            } else {
2283                let name = child.utf8_text(source).unwrap_or("");
2284                (name, name)
2285            };
2286
2287            if original.is_empty() {
2288                continue;
2289            }
2290
2291            // Resolve against symbol table, preferring entities from the source module
2292            if let Some(target_ids) = symbol_table.get(original) {
2293                let target = target_ids.iter().find(|id| {
2294                    entity_map.get(*id).map_or(false, |e| {
2295                        let stem = e.file_path.rsplit('/').next().unwrap_or(&e.file_path);
2296                        let stem = stem
2297                            .strip_suffix(".py")
2298                            .or_else(|| stem.strip_suffix(".rs"))
2299                            .or_else(|| stem.strip_suffix(".ts"))
2300                            .or_else(|| stem.strip_suffix(".js"))
2301                            .unwrap_or(stem);
2302                        stem == source_module
2303                    })
2304                });
2305
2306                if let Some(target_id) = target {
2307                    import_table.insert(
2308                        (file_path.to_string(), local.to_string()),
2309                        target_id.clone(),
2310                    );
2311                    // Also add to module scope definitions
2312                    if !scopes.is_empty() {
2313                        scopes[0]
2314                            .defs
2315                            .insert(local.to_string(), target_id.clone());
2316                    }
2317                }
2318            }
2319        }
2320    }
2321}
2322
2323/// Collect ALL AST references in a file with a single tree walk.
2324/// Each ref records its row so callers can bucket refs into entities by line range.
2325fn collect_all_file_refs(
2326    root: tree_sitter::Node,
2327    source: &[u8],
2328    config: &ScopeResolveConfig,
2329) -> Vec<AstRef> {
2330    let mut refs = Vec::new();
2331    let mut worklist = vec![root];
2332    while let Some(node) = worklist.pop() {
2333        let node_row = node.start_position().row;
2334        let kind = node.kind();
2335
2336        // Call nodes (e.g. "call", "call_expression", "method_invocation")
2337        if config.call_nodes.contains(&kind) {
2338            match &config.call_style {
2339                CallNodeStyle::FunctionField(field) => {
2340                    if let Some(func) = node.child_by_field_name(field) {
2341                        // Pass empty entity_name — self-ref filtering is done at resolution time
2342                        extract_call_ref(func, "", "", source, &mut refs, config, node_row);
2343                    }
2344                }
2345                CallNodeStyle::DirectMethod { object_field, method_field } => {
2346                    let method_name = node.child_by_field_name(method_field)
2347                        .and_then(|n| n.utf8_text(source).ok())
2348                        .unwrap_or("");
2349                    if !method_name.is_empty() && !is_builtin(method_name, config) {
2350                        if let Some(obj_node) = node.child_by_field_name(object_field) {
2351                            let receiver = obj_node.utf8_text(source).unwrap_or("").to_string();
2352                            let receiver = receiver.trim_end_matches('.').to_string();
2353                            refs.push(AstRef {
2354                                kind: AstRefKind::MethodCall { receiver, method: method_name.to_string() },
2355                                row: node_row,
2356                            });
2357                        } else {
2358                            refs.push(AstRef {
2359                                kind: AstRefKind::Call(method_name.to_string()),
2360                                row: node_row,
2361                            });
2362                        }
2363                    }
2364                }
2365            }
2366            let mut cursor = node.walk();
2367            let children: Vec<_> = node.named_children(&mut cursor).collect();
2368            for child in children.into_iter().rev() {
2369                worklist.push(child);
2370            }
2371            continue;
2372        }
2373
2374        // New expression nodes (e.g. "new_expression", "object_creation_expression")
2375        if config.new_expr_nodes.contains(&kind) {
2376            if let Some(type_node) = node.child_by_field_name(config.new_expr_type_field) {
2377                let name = type_node.utf8_text(source).unwrap_or("");
2378                let name = name.rsplit('.').next().unwrap_or(name);
2379                if !name.is_empty() && !is_builtin(name, config) {
2380                    refs.push(AstRef {
2381                        kind: AstRefKind::Call(name.to_string()),
2382                        row: node_row,
2383                    });
2384                }
2385            }
2386            let mut cursor = node.walk();
2387            let children: Vec<_> = node.named_children(&mut cursor).collect();
2388            for child in children.into_iter().rev() {
2389                worklist.push(child);
2390            }
2391            continue;
2392        }
2393
2394        // Composite literal nodes (e.g. Go "composite_literal")
2395        if config.composite_literal_nodes.contains(&kind) {
2396            if let Some(type_node) = node.child_by_field_name("type") {
2397                let name = type_node.utf8_text(source).unwrap_or("");
2398                if name.chars().next().map_or(false, |c| c.is_uppercase())
2399                    && !is_builtin(name, config)
2400                {
2401                    refs.push(AstRef {
2402                        kind: AstRefKind::Call(name.to_string()),
2403                        row: node_row,
2404                    });
2405                }
2406            }
2407        }
2408
2409        // Recurse into children
2410        let mut cursor = node.walk();
2411        let children: Vec<_> = node.named_children(&mut cursor).collect();
2412        for child in children.into_iter().rev() {
2413            worklist.push(child);
2414        }
2415    }
2416    refs
2417}
2418
2419/// Extract a call reference from a function/callee node (shared across languages)
2420fn extract_call_ref(
2421    func: tree_sitter::Node,
2422    _entity_id: &str,
2423    entity_name: &str,
2424    source: &[u8],
2425    refs: &mut Vec<AstRef>,
2426    config: &ScopeResolveConfig,
2427    row: usize,
2428) {
2429    let func_kind = func.kind();
2430
2431    if func_kind == "identifier" {
2432        let name = func.utf8_text(source).unwrap_or("");
2433        if !name.is_empty() && name != entity_name && !is_builtin(name, config) {
2434            refs.push(AstRef {
2435                kind: AstRefKind::Call(name.to_string()),
2436                row,
2437            });
2438        }
2439        return;
2440    }
2441
2442    // Check config member_access patterns
2443    for ma in config.member_access {
2444        if func_kind == ma.node_kind {
2445            extract_member_call_ref(func, ma.object_field, ma.property_field, source, refs, row);
2446            return;
2447        }
2448    }
2449
2450    // Scoped call nodes (e.g. Rust "scoped_identifier" for Type::method)
2451    if config.scoped_call_nodes.contains(&func_kind) {
2452        let text = func.utf8_text(source).unwrap_or("");
2453        let parts: Vec<&str> = text.split("::").collect();
2454        if parts.len() >= 2 {
2455            let type_name = parts[parts.len() - 2];
2456            let method_name = parts[parts.len() - 1];
2457            if !type_name.is_empty() && !method_name.is_empty() {
2458                refs.push(AstRef {
2459                    kind: AstRefKind::Call(method_name.to_string()),
2460                    row,
2461                });
2462                if type_name.chars().next().map_or(false, |c| c.is_uppercase())
2463                    && !is_builtin(type_name, config)
2464                {
2465                    refs.push(AstRef {
2466                        kind: AstRefKind::Call(type_name.to_string()),
2467                        row,
2468                    });
2469                }
2470            }
2471        }
2472    }
2473}
2474
2475/// Extract a member/method call from a node with object+property fields
2476fn extract_member_call_ref(
2477    node: tree_sitter::Node,
2478    object_field: &str,
2479    attr_field: &str,
2480    source: &[u8],
2481    refs: &mut Vec<AstRef>,
2482    row: usize,
2483) {
2484    let obj = node
2485        .child_by_field_name(object_field)
2486        .and_then(|n| n.utf8_text(source).ok())
2487        .unwrap_or("");
2488    let attr = node
2489        .child_by_field_name(attr_field)
2490        .and_then(|n| n.utf8_text(source).ok())
2491        .unwrap_or("");
2492    if !obj.is_empty() && !attr.is_empty() {
2493        push_method_call_ref(obj, attr, refs, row);
2494    }
2495}
2496
2497fn push_method_call_ref(obj: &str, method: &str, refs: &mut Vec<AstRef>, row: usize) {
2498    refs.push(AstRef {
2499        kind: AstRefKind::MethodCall {
2500            receiver: obj.to_string(),
2501            method: method.to_string(),
2502        },
2503        row,
2504    });
2505}
2506
2507/// Resolve a single reference against scopes and symbol tables.
2508fn resolve_ref(
2509    ast_ref: &AstRef,
2510    scope_idx: usize,
2511    scopes: &[Scope],
2512    symbol_table: &HashMap<String, Vec<String>>,
2513    class_members: &HashMap<String, Vec<(String, String)>>,
2514    import_table: &HashMap<(String, String), String>,
2515    instance_attr_types: &HashMap<(String, String), String>,
2516    entity_map: &HashMap<String, EntityInfo>,
2517    file_path: &str,
2518    from_entity_id: &str,
2519) -> Option<(String, RefType, &'static str)> {
2520    match &ast_ref.kind {
2521        AstRefKind::Call(name) => {
2522            // 1. Walk scope chain for the name
2523            if let Some(eid) = lookup_scope_chain(scope_idx, scopes, name) {
2524                if eid != from_entity_id {
2525                    return Some((eid, RefType::Calls, "scope_chain"));
2526                }
2527            }
2528
2529            // 2. Check import table
2530            let key = (file_path.to_string(), name.clone());
2531            if let Some(target_id) = import_table.get(&key) {
2532                return Some((target_id.clone(), RefType::Calls, "import"));
2533            }
2534
2535            // 3. Global symbol table fallback (constructor calls or cross-file functions)
2536            if let Some(target_ids) = symbol_table.get(name.as_str()) {
2537                let is_constructor = name.chars().next().map_or(false, |c| c.is_uppercase());
2538                let ref_type = if is_constructor { RefType::TypeRef } else { RefType::Calls };
2539                // Prefer same-file, then any
2540                let target = target_ids
2541                    .iter()
2542                    .find(|id| {
2543                        entity_map
2544                            .get(*id)
2545                            .map_or(false, |e| e.file_path == file_path)
2546                    })
2547                    .or_else(|| {
2548                        // For lowercase calls, only fall back to global if there's exactly one match
2549                        // (avoid ambiguous resolution that bag-of-words creates)
2550                        if is_constructor || target_ids.len() == 1 {
2551                            target_ids.first()
2552                        } else {
2553                            None
2554                        }
2555                    });
2556                if let Some(tid) = target {
2557                    return Some((tid.clone(), ref_type, "scope_chain"));
2558                }
2559            }
2560
2561            None
2562        }
2563
2564        AstRefKind::MethodCall { receiver, method } => {
2565            if receiver == "self" || receiver == "this" {
2566                // self.method() -> find in enclosing class
2567                let mut idx = scope_idx;
2568                loop {
2569                    if scopes[idx].kind == "class" {
2570                        if let Some(eid) = scopes[idx].defs.get(method.as_str()) {
2571                            return Some((eid.clone(), RefType::Calls, "scope_chain"));
2572                        }
2573                        break;
2574                    }
2575                    match scopes[idx].parent {
2576                        Some(p) => idx = p,
2577                        None => break,
2578                    }
2579                }
2580                return None;
2581            }
2582
2583            // Handle chained self.attr.method() pattern
2584            // receiver is "self.X" where X is an instance attribute
2585            if receiver.starts_with("self.") || receiver.starts_with("this.") {
2586                let attr_name = &receiver[5..]; // strip "self." or "this."
2587                // Find the enclosing class name
2588                let class_name = find_enclosing_class(scope_idx, scopes, entity_map);
2589                if let Some(cn) = class_name {
2590                    // Look up instance attribute type
2591                    if let Some(attr_type) = instance_attr_types.get(&(cn, attr_name.to_string())) {
2592                        if let Some(members) = class_members.get(attr_type.as_str()) {
2593                            if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2594                                return Some((mid.clone(), RefType::Calls, "type_tracking"));
2595                            }
2596                        }
2597                    }
2598                }
2599            }
2600
2601            // Handle chained var.field.method() pattern (e.g. Go receiver: t.Conn.Execute())
2602            if receiver.contains('.') && !receiver.starts_with("self.") && !receiver.starts_with("this.") {
2603                if let Some(dot_pos) = receiver.find('.') {
2604                    let var_part = &receiver[..dot_pos];
2605                    let field_part = &receiver[dot_pos + 1..];
2606                    if let Some(var_type) = lookup_type_in_scopes(scope_idx, scopes, var_part) {
2607                        if let Some(attr_type) = instance_attr_types.get(&(var_type, field_part.to_string())) {
2608                            if let Some(members) = class_members.get(attr_type.as_str()) {
2609                                if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2610                                    return Some((mid.clone(), RefType::Calls, "type_tracking"));
2611                                }
2612                            }
2613                        }
2614                    }
2615                }
2616            }
2617
2618            // receiver.method() -> look up receiver type, then resolve method
2619            let receiver_type = lookup_type_in_scopes(scope_idx, scopes, receiver);
2620
2621            if let Some(class_name) = receiver_type {
2622                if let Some(members) = class_members.get(class_name.as_str()) {
2623                    if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2624                        return Some((mid.clone(), RefType::Calls, "type_tracking"));
2625                    }
2626                }
2627            }
2628
2629            // Fallback: check import table for the receiver
2630            let key = (file_path.to_string(), receiver.clone());
2631            if let Some(target_id) = import_table.get(&key) {
2632                if let Some(info) = entity_map.get(target_id) {
2633                    if matches!(info.entity_type.as_str(), "class" | "struct") {
2634                        if let Some(members) = class_members.get(&info.name) {
2635                            if let Some((_, mid)) =
2636                                members.iter().find(|(n, _)| n == method)
2637                            {
2638                                return Some((
2639                                    mid.clone(),
2640                                    RefType::Calls,
2641                                    "type_tracking",
2642                                ));
2643                            }
2644                        }
2645                    }
2646                }
2647            }
2648
2649            // Go package-qualified call: package.Function()
2650            // Try the method name directly in the import table
2651            let key = (file_path.to_string(), method.clone());
2652            if let Some(target_id) = import_table.get(&key) {
2653                return Some((target_id.clone(), RefType::Calls, "import"));
2654            }
2655
2656            None
2657        }
2658    }
2659}
2660
2661/// Find the class name for the enclosing class scope.
2662fn find_enclosing_class(
2663    start_scope: usize,
2664    scopes: &[Scope],
2665    entity_map: &HashMap<String, EntityInfo>,
2666) -> Option<String> {
2667    let mut idx = start_scope;
2668    loop {
2669        if scopes[idx].kind == "class" {
2670            if let Some(ref oid) = scopes[idx].owner_id {
2671                return entity_map.get(oid).map(|e| e.name.clone());
2672            }
2673        }
2674        match scopes[idx].parent {
2675            Some(p) => idx = p,
2676            None => return None,
2677        }
2678    }
2679}
2680
2681/// Walk up the scope chain looking for a definition.
2682fn lookup_scope_chain(
2683    start_scope: usize,
2684    scopes: &[Scope],
2685    name: &str,
2686) -> Option<String> {
2687    let mut idx = start_scope;
2688    loop {
2689        if let Some(eid) = scopes[idx].defs.get(name) {
2690            return Some(eid.clone());
2691        }
2692        match scopes[idx].parent {
2693            Some(p) => idx = p,
2694            None => return None,
2695        }
2696    }
2697}
2698
2699/// Walk up the scope chain looking for a type binding.
2700fn lookup_type_in_scopes(
2701    start_scope: usize,
2702    scopes: &[Scope],
2703    var_name: &str,
2704) -> Option<String> {
2705    let mut idx = start_scope;
2706    loop {
2707        if let Some(type_name) = scopes[idx].types.get(var_name) {
2708            return Some(type_name.clone());
2709        }
2710        match scopes[idx].parent {
2711            Some(p) => idx = p,
2712            None => return None,
2713        }
2714    }
2715}
2716
2717fn is_builtin(name: &str, config: &ScopeResolveConfig) -> bool {
2718    // Common builtins across languages
2719    if matches!(name, "None" | "True" | "False" | "null" | "undefined" | "nil") {
2720        return true;
2721    }
2722    config.builtins.contains(&name)
2723}