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