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        // Rust mod_item: create a module scope so nested functions resolve
608        // names from the parent scope (e.g. super::target() walks up correctly).
609        if kind == "mod_item" {
610            let mod_name = node
611                .child_by_field_name("name")
612                .and_then(|n| n.utf8_text(source).ok())
613                .unwrap_or("");
614            let mod_scope_idx = scopes.len();
615            scopes.push(Scope {
616                parent: Some(current_scope),
617                defs: HashMap::new(),
618                types: HashMap::new(),
619                pending_call_types: HashMap::new(),
620                owner_id: None,
621                kind: "module",
622            });
623
624            // Register any entities that are children of this module
625            let mod_entity = file_entities.iter().find(|e| {
626                e.name == mod_name
627                    && e.entity_type == "module"
628                    && {
629                        let line = node.start_position().row + 1;
630                        e.start_line <= line && line <= e.end_line
631                    }
632            }).copied();
633
634            if let Some(me) = mod_entity {
635                scopes[mod_scope_idx].owner_id = Some(me.id.clone());
636                entity_scope_map.entry(me.id.clone()).or_insert(current_scope);
637                entity_inner_scope.insert(me.id.clone(), mod_scope_idx);
638
639                // Register child entities in the module scope
640                if let Some(children) = children_by_parent.get(me.id.as_str()) {
641                    for child_entity in children {
642                        scopes[mod_scope_idx]
643                            .defs
644                            .insert(child_entity.name.clone(), child_entity.id.clone());
645                        entity_scope_map.insert(child_entity.id.clone(), mod_scope_idx);
646                    }
647                }
648            }
649
650            let mut cursor = node.walk();
651            let children: Vec<_> = node.named_children(&mut cursor).collect();
652            for child in children.into_iter().rev() {
653                worklist.push((child, mod_scope_idx));
654            }
655            continue;
656        }
657
658        // Function-like scope: config-driven
659        let is_function_like = config.function_scope_nodes.contains(&kind);
660
661        if is_function_like {
662            let func_name = node.child_by_field_name("name")
663                .and_then(|n| n.utf8_text(source).ok())
664                .unwrap_or("");
665
666            let parent_scope = if config.external_method && kind == "method_declaration" {
667                let receiver_type = node.utf8_text(source).ok()
668                    .and_then(|t| extract_go_receiver_type(t));
669                if let Some(ref struct_name) = receiver_type {
670                    let found = scopes.iter().enumerate().find(|(_, s)| {
671                        s.kind == "class" && s.owner_id.as_ref().map_or(false, |oid| {
672                            entity_map.get(oid).map_or(false, |e| e.name == *struct_name)
673                        })
674                    });
675                    found.map(|(idx, _)| idx).unwrap_or(current_scope)
676                } else {
677                    current_scope
678                }
679            } else {
680                current_scope
681            };
682
683            let func_scope_idx = scopes.len();
684            scopes.push(Scope {
685                parent: Some(parent_scope),
686                defs: HashMap::new(),
687                types: HashMap::new(),
688                pending_call_types: HashMap::new(),
689                owner_id: None,
690                kind: "function",
691            });
692
693            let func_entity = file_entities.iter().find(|e| {
694                e.name == func_name && {
695                    let line = node.start_position().row + 1;
696                    e.start_line <= line && line <= e.end_line
697                }
698            }).copied();
699
700            if let Some(fe) = func_entity {
701                scopes[func_scope_idx].owner_id = Some(fe.id.clone());
702                entity_scope_map.entry(fe.id.clone()).or_insert(parent_scope);
703                entity_inner_scope.insert(fe.id.clone(), func_scope_idx);
704                if config.external_method && kind == "method_declaration" && parent_scope != current_scope {
705                    scopes[parent_scope].defs.insert(fe.name.clone(), fe.id.clone());
706                }
707            }
708
709            scan_assignments(node, func_scope_idx, scopes, source, config);
710            scan_function_params(node, func_scope_idx, scopes, source, config);
711
712            if config.external_method && kind == "method_declaration" {
713                if let Some(receiver) = node.child_by_field_name("receiver") {
714                    let mut rcursor = receiver.walk();
715                    for param in receiver.named_children(&mut rcursor) {
716                        if param.kind() == "parameter_declaration" {
717                            let param_name = param
718                                .child_by_field_name("name")
719                                .and_then(|n| n.utf8_text(source).ok())
720                                .unwrap_or("");
721                            let param_type = param
722                                .child_by_field_name("type")
723                                .map(|n| extract_base_type(n, source))
724                                .unwrap_or_default();
725                            if !param_name.is_empty() && !param_type.is_empty() {
726                                scopes[func_scope_idx]
727                                    .types
728                                    .insert(param_name.to_string(), param_type);
729                            }
730                        }
731                    }
732                }
733            }
734
735            let mut cursor = node.walk();
736            let children: Vec<_> = node.named_children(&mut cursor).collect();
737            for child in children.into_iter().rev() {
738                worklist.push((child, func_scope_idx));
739            }
740            continue;
741        }
742
743        let mut cursor = node.walk();
744        let children: Vec<_> = node.named_children(&mut cursor).collect();
745        for child in children.into_iter().rev() {
746            worklist.push((child, current_scope));
747        }
748    }
749}
750
751/// Scan for variable assignments and record type bindings.
752fn scan_assignments(
753    root: tree_sitter::Node,
754    scope_idx: usize,
755    scopes: &mut Vec<Scope>,
756    source: &[u8],
757    config: &ScopeResolveConfig,
758) {
759    let mut worklist = vec![root];
760    while let Some(node) = worklist.pop() {
761        let mut cursor = node.walk();
762        for child in node.named_children(&mut cursor) {
763            let ck = child.kind();
764
765            // Check if this node matches an assignment rule
766            for rule in config.assignment_rules {
767                if ck == rule.node_kind {
768                    match rule.strategy {
769                        AssignmentStrategy::LeftRight => {
770                            scan_single_assignment(child, scope_idx, scopes, source);
771                        }
772                        AssignmentStrategy::Declarators => {
773                            scan_ts_var_declaration(child, scope_idx, scopes, source);
774                        }
775                        AssignmentStrategy::PatternBased => {
776                            scan_rust_let_declaration(child, scope_idx, scopes, source);
777                        }
778                        AssignmentStrategy::ShortVar => {
779                            scan_go_short_var(child, scope_idx, scopes, source);
780                        }
781                        AssignmentStrategy::VarSpec => {
782                            scan_go_var_declaration(child, scope_idx, scopes, source);
783                        }
784                    }
785                }
786            }
787
788            // Recurse into configured container nodes
789            if config.assignment_recurse_into.contains(&ck) {
790                worklist.push(child);
791            }
792        }
793    }
794}
795
796/// Scan function parameter type annotations and add them as type bindings.
797/// e.g. `def foo(shelter: Shelter)` -> types["shelter"] = "Shelter"
798fn scan_function_params(
799    node: tree_sitter::Node,
800    scope_idx: usize,
801    scopes: &mut Vec<Scope>,
802    source: &[u8],
803    config: &ScopeResolveConfig,
804) {
805    let params_node = match node.child_by_field_name("parameters") {
806        Some(p) => p,
807        None => return,
808    };
809
810    let mut cursor = params_node.walk();
811    for child in params_node.named_children(&mut cursor) {
812        for rule in config.param_rules {
813            if child.kind() != rule.node_kind {
814                continue;
815            }
816
817            let param_name = match &rule.name_field {
818                ParamNameField::Simple(field) => {
819                    child.child_by_field_name(field)
820                        .and_then(|n| n.utf8_text(source).ok())
821                        .unwrap_or("")
822                }
823                ParamNameField::WithFallback(field) => {
824                    child.child_by_field_name(field)
825                        .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
826                        .and_then(|n| n.utf8_text(source).ok())
827                        .unwrap_or("")
828                }
829                ParamNameField::RustPattern => {
830                    child.child_by_field_name("pattern")
831                        .and_then(|n| {
832                            if n.kind() == "identifier" {
833                                n.utf8_text(source).ok()
834                            } else if n.kind() == "mut_pattern" {
835                                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
836                            } else if n.kind() == "reference_pattern" {
837                                n.named_child(0).and_then(|c| {
838                                    if c.kind() == "identifier" {
839                                        c.utf8_text(source).ok()
840                                    } else if c.kind() == "mut_pattern" {
841                                        c.named_child(0).and_then(|cc| cc.utf8_text(source).ok())
842                                    } else {
843                                        None
844                                    }
845                                })
846                            } else {
847                                None
848                            }
849                        })
850                        .unwrap_or("")
851                }
852            };
853
854            if param_name.is_empty() || rule.skip_names.contains(&param_name) {
855                continue;
856            }
857
858            if let Some(type_node) = child.child_by_field_name(rule.type_field) {
859                let type_text = extract_base_type(type_node, source);
860                if !type_text.is_empty()
861                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
862                {
863                    scopes[scope_idx]
864                        .types
865                        .insert(param_name.to_string(), type_text);
866                }
867            }
868        }
869    }
870}
871
872/// Python/TS: `x = Foo()` or `x = func()`
873fn scan_single_assignment(
874    node: tree_sitter::Node,
875    scope_idx: usize,
876    scopes: &mut Vec<Scope>,
877    source: &[u8],
878) {
879    let assign = if node.kind() == "assignment" {
880        node
881    } else {
882        let mut cursor = node.walk();
883        let children: Vec<_> = node.named_children(&mut cursor).collect();
884        match children.into_iter().find(|c| c.kind() == "assignment" || c.kind() == "assignment_expression") {
885            Some(a) => a,
886            None => return,
887        }
888    };
889
890    let left = match assign.child_by_field_name("left") {
891        Some(l) => l,
892        None => return,
893    };
894    let right = match assign.child_by_field_name("right") {
895        Some(r) => r,
896        None => return,
897    };
898
899    if left.kind() != "identifier" {
900        return;
901    }
902    let var_name = match left.utf8_text(source) {
903        Ok(n) => n.to_string(),
904        Err(_) => return,
905    };
906
907    record_type_from_rhs(right, &var_name, scope_idx, scopes, source);
908}
909
910/// TS: `const x = new Foo()` or `const x: Type = ...` or `const x = func()`
911fn scan_ts_var_declaration(
912    node: tree_sitter::Node,
913    scope_idx: usize,
914    scopes: &mut Vec<Scope>,
915    source: &[u8],
916) {
917    let mut cursor = node.walk();
918    for child in node.named_children(&mut cursor) {
919        if child.kind() == "variable_declarator" {
920            let var_name = child
921                .child_by_field_name("name")
922                .and_then(|n| n.utf8_text(source).ok())
923                .unwrap_or("")
924                .to_string();
925            if var_name.is_empty() {
926                continue;
927            }
928
929            // Check for explicit type annotation: `const x: Foo = ...`
930            if let Some(type_ann) = child.child_by_field_name("type") {
931                let type_text = extract_base_type(type_ann, source);
932                if !type_text.is_empty()
933                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
934                {
935                    scopes[scope_idx]
936                        .types
937                        .insert(var_name.clone(), type_text);
938                    continue;
939                }
940            }
941
942            // Check RHS value
943            if let Some(value) = child.child_by_field_name("value") {
944                record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
945            }
946        }
947    }
948}
949
950/// Rust: `let x: Type = ...` or `let x = Foo::new()`
951fn scan_rust_let_declaration(
952    node: tree_sitter::Node,
953    scope_idx: usize,
954    scopes: &mut Vec<Scope>,
955    source: &[u8],
956) {
957    let var_name = node
958        .child_by_field_name("pattern")
959        .and_then(|n| {
960            // Pattern can be just an identifier or `mut x`
961            if n.kind() == "identifier" {
962                n.utf8_text(source).ok()
963            } else if n.kind() == "mut_pattern" {
964                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
965            } else {
966                None
967            }
968        })
969        .unwrap_or("")
970        .to_string();
971
972    if var_name.is_empty() {
973        return;
974    }
975
976    // Check for explicit type annotation: `let x: Connection = ...`
977    if let Some(type_node) = node.child_by_field_name("type") {
978        let type_text = extract_base_type(type_node, source);
979        if !type_text.is_empty()
980            && type_text.chars().next().map_or(false, |c| c.is_uppercase())
981        {
982            scopes[scope_idx]
983                .types
984                .insert(var_name, type_text);
985            return;
986        }
987    }
988
989    // Check RHS value
990    if let Some(value) = node.child_by_field_name("value") {
991        record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
992    }
993}
994
995/// Go: `x := Foo{}` or `x := NewFoo()`
996fn scan_go_short_var(
997    node: tree_sitter::Node,
998    scope_idx: usize,
999    scopes: &mut Vec<Scope>,
1000    source: &[u8],
1001) {
1002    let left = match node.child_by_field_name("left") {
1003        Some(l) => l,
1004        None => return,
1005    };
1006    let right = match node.child_by_field_name("right") {
1007        Some(r) => r,
1008        None => return,
1009    };
1010
1011    // left is expression_list, right is expression_list
1012    let var_name = if left.kind() == "expression_list" {
1013        left.named_child(0)
1014            .and_then(|n| n.utf8_text(source).ok())
1015            .unwrap_or("")
1016            .to_string()
1017    } else {
1018        left.utf8_text(source).unwrap_or("").to_string()
1019    };
1020
1021    if var_name.is_empty() {
1022        return;
1023    }
1024
1025    let rhs = if right.kind() == "expression_list" {
1026        match right.named_child(0) {
1027            Some(n) => n,
1028            None => return,
1029        }
1030    } else {
1031        right
1032    };
1033
1034    record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
1035}
1036
1037/// Go: `var x Type = ...` or `var x = Foo{}`
1038fn scan_go_var_declaration(
1039    node: tree_sitter::Node,
1040    scope_idx: usize,
1041    scopes: &mut Vec<Scope>,
1042    source: &[u8],
1043) {
1044    let mut cursor = node.walk();
1045    for child in node.named_children(&mut cursor) {
1046        if child.kind() == "var_spec" {
1047            let var_name = child
1048                .child_by_field_name("name")
1049                .and_then(|n| n.utf8_text(source).ok())
1050                .unwrap_or("")
1051                .to_string();
1052            if var_name.is_empty() {
1053                // Try first named child as name
1054                if let Some(first) = child.named_child(0) {
1055                    if first.kind() == "identifier" {
1056                        let name = first.utf8_text(source).unwrap_or("").to_string();
1057                        if !name.is_empty() {
1058                            // Check for type child
1059                            if let Some(type_node) = child.child_by_field_name("type") {
1060                                let type_text = extract_base_type(type_node, source);
1061                                if !type_text.is_empty()
1062                                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1063                                {
1064                                    scopes[scope_idx].types.insert(name, type_text);
1065                                }
1066                            }
1067                        }
1068                    }
1069                }
1070                continue;
1071            }
1072
1073            // Check for explicit type
1074            if let Some(type_node) = child.child_by_field_name("type") {
1075                let type_text = extract_base_type(type_node, source);
1076                if !type_text.is_empty()
1077                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1078                {
1079                    scopes[scope_idx]
1080                        .types
1081                        .insert(var_name, type_text);
1082                    continue;
1083                }
1084            }
1085
1086            // Check RHS value
1087            if let Some(value) = child.child_by_field_name("value") {
1088                let rhs = if value.kind() == "expression_list" {
1089                    value.named_child(0).unwrap_or(value)
1090                } else {
1091                    value
1092                };
1093                record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
1094            }
1095        }
1096    }
1097}
1098
1099/// Record type binding from a RHS expression (works for all languages).
1100/// Handles: constructor calls, new expressions, struct literals, function calls.
1101fn record_type_from_rhs(
1102    rhs: tree_sitter::Node,
1103    var_name: &str,
1104    scope_idx: usize,
1105    scopes: &mut Vec<Scope>,
1106    source: &[u8],
1107) {
1108    match rhs.kind() {
1109        // Python/Go: Foo() or func()
1110        "call" | "call_expression" => {
1111            let func_node = rhs
1112                .child_by_field_name("function")
1113                .or_else(|| rhs.named_child(0));
1114            if let Some(func) = func_node {
1115                if func.kind() == "identifier" {
1116                    let name = func.utf8_text(source).unwrap_or("");
1117                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1118                        scopes[scope_idx]
1119                            .types
1120                            .insert(var_name.to_string(), name.to_string());
1121                    } else {
1122                        scopes[scope_idx]
1123                            .pending_call_types
1124                            .insert(var_name.to_string(), name.to_string());
1125                    }
1126                }
1127                // Rust: Type::new() / Type::from() etc.
1128                if func.kind() == "scoped_identifier" {
1129                    let text = func.utf8_text(source).unwrap_or("");
1130                    let parts: Vec<&str> = text.split("::").collect();
1131                    if parts.len() >= 2 {
1132                        let type_name = parts[0];
1133                        let method_name = parts[parts.len() - 1];
1134                        if type_name.chars().next().map_or(false, |c| c.is_uppercase()) {
1135                            scopes[scope_idx]
1136                                .types
1137                                .insert(var_name.to_string(), type_name.to_string());
1138                        } else {
1139                            scopes[scope_idx]
1140                                .pending_call_types
1141                                .insert(var_name.to_string(), method_name.to_string());
1142                        }
1143                    }
1144                }
1145                // Go: package.NewFoo() or package.GetFoo()
1146                if func.kind() == "selector_expression" {
1147                    let field = func
1148                        .child_by_field_name("field")
1149                        .and_then(|n| n.utf8_text(source).ok())
1150                        .unwrap_or("");
1151                    // Go convention: NewFoo() returns *Foo
1152                    if let Some(type_name) = field.strip_prefix("New") {
1153                        if !type_name.is_empty()
1154                            && type_name.chars().next().map_or(false, |c| c.is_uppercase())
1155                        {
1156                            scopes[scope_idx]
1157                                .types
1158                                .insert(var_name.to_string(), type_name.to_string());
1159                        }
1160                    } else if field.starts_with("Get") || field.chars().next().map_or(false, |c| c.is_uppercase()) {
1161                        // Other Go package functions: record for return type resolution
1162                        scopes[scope_idx]
1163                            .pending_call_types
1164                            .insert(var_name.to_string(), field.to_string());
1165                    }
1166                }
1167            }
1168        }
1169        // TS: new Foo()
1170        "new_expression" => {
1171            if let Some(constructor) = rhs.child_by_field_name("constructor") {
1172                let name = constructor.utf8_text(source).unwrap_or("");
1173                if !name.is_empty() {
1174                    scopes[scope_idx]
1175                        .types
1176                        .insert(var_name.to_string(), name.to_string());
1177                }
1178            }
1179        }
1180        // Go: Foo{} (composite_literal / struct literal)
1181        "composite_literal" => {
1182            if let Some(type_node) = rhs.child_by_field_name("type") {
1183                let name = type_node.utf8_text(source).unwrap_or("");
1184                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1185                    scopes[scope_idx]
1186                        .types
1187                        .insert(var_name.to_string(), name.to_string());
1188                }
1189            }
1190        }
1191        _ => {}
1192    }
1193}
1194
1195/// Extract the base type name from a type annotation node.
1196/// Strips pointers, references, generics to get just the type name.
1197fn extract_base_type(type_node: tree_sitter::Node, source: &[u8]) -> String {
1198    let text = type_node.utf8_text(source).unwrap_or("").trim().to_string();
1199    // Strip reference/pointer prefixes and mut keyword
1200    let text = text.trim_start_matches('&').trim_start_matches('*');
1201    let text = text.strip_prefix("mut ").unwrap_or(text).trim_start();
1202    // Strip generic parameters (angle brackets and Python-style square brackets)
1203    let text = if let Some(i) = text.find('<') {
1204        &text[..i]
1205    } else if let Some(i) = text.find('[') {
1206        &text[..i]
1207    } else {
1208        text
1209    };
1210    // Strip lifetime annotations for Rust
1211    let text = text.trim();
1212    // For type_annotation nodes in TS, strip the leading `: `
1213    let text = text.trim_start_matches(':').trim();
1214    text.to_string()
1215}
1216
1217/// Parse Go receiver type from method content: `func (r *ReceiverType) Name(...)`
1218pub fn extract_go_receiver_type(content: &str) -> Option<String> {
1219    let after_func = content.strip_prefix("func")?.trim_start();
1220    let paren_start = after_func.find('(')?;
1221    let paren_end = after_func.find(')')?;
1222    let receiver_block = &after_func[paren_start + 1..paren_end];
1223    // Could be: "r ReceiverType", "r *ReceiverType", "*ReceiverType"
1224    let parts: Vec<&str> = receiver_block.split_whitespace().collect();
1225    let type_str = parts.last()?;
1226    let name = type_str.trim_start_matches('*');
1227    if name.is_empty() {
1228        None
1229    } else {
1230        Some(name.to_string())
1231    }
1232}
1233
1234/// Build Go package index: pkg_name → [(entity_name, entity_id)]
1235/// Maps file stems and parent directory names to entities for O(1) package import lookup.
1236fn build_go_pkg_index(
1237    symbol_table: &HashMap<String, Vec<String>>,
1238    entity_map: &HashMap<String, EntityInfo>,
1239) -> HashMap<String, Vec<(String, String)>> {
1240    let mut idx: HashMap<String, Vec<(String, String)>> = HashMap::new();
1241    for (name, target_ids) in symbol_table.iter() {
1242        for target_id in target_ids {
1243            if let Some(entity) = entity_map.get(target_id) {
1244                if !entity.file_path.ends_with(".go") {
1245                    continue;
1246                }
1247                let file_stem = entity.file_path.rsplit('/').next().unwrap_or(&entity.file_path);
1248                let file_stem = file_stem.strip_suffix(".go").unwrap_or(file_stem);
1249                idx.entry(file_stem.to_string())
1250                    .or_default()
1251                    .push((name.clone(), target_id.clone()));
1252                if let Some(parent_start) = entity.file_path.rfind('/') {
1253                    let parent_path = &entity.file_path[..parent_start];
1254                    if let Some(dir_name_start) = parent_path.rfind('/') {
1255                        let dir_name = &parent_path[dir_name_start + 1..];
1256                        if dir_name != file_stem {
1257                            idx.entry(dir_name.to_string())
1258                                .or_default()
1259                                .push((name.clone(), target_id.clone()));
1260                        }
1261                    } else if !parent_path.is_empty() && parent_path != file_stem {
1262                        idx.entry(parent_path.to_string())
1263                            .or_default()
1264                            .push((name.clone(), target_id.clone()));
1265                    }
1266                }
1267            }
1268        }
1269    }
1270    idx
1271}
1272
1273/// Scan function bodies/signatures for return types to build a return type map.
1274fn scan_return_types(
1275    root: tree_sitter::Node,
1276    _file_path: &str,
1277    file_entities: &[&SemanticEntity],
1278    source: &[u8],
1279    return_type_map: &mut HashMap<String, String>,
1280    config: &ScopeResolveConfig,
1281) {
1282    let mut worklist = vec![root];
1283    while let Some(node) = worklist.pop() {
1284        let kind = node.kind();
1285
1286        let is_func = config.function_scope_nodes.contains(&kind);
1287
1288        if is_func {
1289            let func_name = node
1290                .child_by_field_name("name")
1291                .and_then(|n| n.utf8_text(source).ok())
1292                .unwrap_or("");
1293
1294            let func_entity = file_entities.iter().find(|e| {
1295                e.name == func_name && {
1296                    let line = node.start_position().row + 1;
1297                    e.start_line <= line && line <= e.end_line
1298                }
1299            }).copied();
1300
1301            if let Some(fe) = func_entity {
1302                // Try explicit return type annotation first
1303                let ret_type = config.return_type_field.and_then(|field| {
1304                    node.child_by_field_name(field)
1305                        .map(|n| extract_base_type(n, source))
1306                        .filter(|t| !t.is_empty() && t.chars().next().map_or(false, |c| c.is_uppercase()))
1307                });
1308
1309                if let Some(rt) = ret_type {
1310                    return_type_map.insert(fe.id.clone(), rt);
1311                } else {
1312                    // Fall back to body heuristic: return ClassName()
1313                    if let Some(ret_type) = find_return_constructor(node, source) {
1314                        return_type_map.insert(fe.id.clone(), ret_type);
1315                    }
1316                }
1317            }
1318        }
1319
1320        let mut cursor = node.walk();
1321        let children: Vec<_> = node.named_children(&mut cursor).collect();
1322        for child in children.into_iter().rev() {
1323            worklist.push(child);
1324        }
1325    }
1326}
1327
1328/// Find `return ClassName()` patterns in a function body (heuristic fallback).
1329fn find_return_constructor(root: tree_sitter::Node, source: &[u8]) -> Option<String> {
1330    let mut worklist = vec![root];
1331    while let Some(node) = worklist.pop() {
1332        let mut cursor = node.walk();
1333        for child in node.named_children(&mut cursor) {
1334            if child.kind() == "return_statement" {
1335                let mut inner_cursor = child.walk();
1336                for ret_child in child.named_children(&mut inner_cursor) {
1337                    // Python: call, TS/Go: call_expression
1338                    if ret_child.kind() == "call" || ret_child.kind() == "call_expression" {
1339                        if let Some(func) = ret_child.child_by_field_name("function") {
1340                            if func.kind() == "identifier" {
1341                                let name = func.utf8_text(source).unwrap_or("");
1342                                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1343                                    return Some(name.to_string());
1344                                }
1345                            }
1346                        }
1347                    }
1348                    // TS: new ClassName()
1349                    if ret_child.kind() == "new_expression" {
1350                        if let Some(constructor) = ret_child.child_by_field_name("constructor") {
1351                            let name = constructor.utf8_text(source).unwrap_or("");
1352                            if !name.is_empty() {
1353                                return Some(name.to_string());
1354                            }
1355                        }
1356                    }
1357                    // Go: StructName{} (composite_literal)
1358                    if ret_child.kind() == "composite_literal" {
1359                        if let Some(type_node) = ret_child.child_by_field_name("type") {
1360                            let name = type_node.utf8_text(source).unwrap_or("");
1361                            if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1362                                return Some(name.to_string());
1363                            }
1364                        }
1365                    }
1366                }
1367            }
1368            // Recurse into blocks
1369            let ck = child.kind();
1370            if ck == "block" || ck == "statement_block" {
1371                worklist.push(child);
1372            }
1373        }
1374    }
1375    None
1376}
1377
1378/// Scan for instance attribute types: __init__ self.attr patterns (Python/TS),
1379/// struct field declarations (Rust/Go).
1380fn scan_init_self_attrs(
1381    root: tree_sitter::Node,
1382    _file_path: &str,
1383    _file_entities: &[&SemanticEntity],
1384    _entity_map: &HashMap<String, EntityInfo>,
1385    source: &[u8],
1386    instance_attr_types: &mut HashMap<(String, String), String>,
1387    init_params_map: &mut HashMap<String, Vec<String>>,
1388    attr_to_param_map: &mut HashMap<(String, String), String>,
1389    config: &ScopeResolveConfig,
1390) {
1391    let mut worklist = vec![root];
1392    while let Some(node) = worklist.pop() {
1393        let kind = node.kind();
1394
1395        match &config.init_strategy {
1396            InitStrategy::ConstructorBody { class_nodes, self_keyword, .. } => {
1397                if class_nodes.contains(&kind) {
1398                    let class_name = node
1399                        .child_by_field_name("name")
1400                        .and_then(|n| n.utf8_text(source).ok())
1401                        .unwrap_or("")
1402                        .to_string();
1403
1404                    if !class_name.is_empty() {
1405                        // Determine lang for scan_class_for_init (it still needs it for TS field scanning)
1406                        let lang = if *self_keyword == "this" { "typescript" } else { "python" };
1407                        scan_class_for_init(node, &class_name, source, instance_attr_types, init_params_map, attr_to_param_map, lang);
1408                    }
1409                }
1410            }
1411            InitStrategy::StructFields { struct_nodes } => {
1412                if struct_nodes.contains(&kind) {
1413                    // Rust struct: extract field types directly
1414                    if kind == "struct_item" {
1415                        let struct_name = node
1416                            .child_by_field_name("name")
1417                            .and_then(|n| n.utf8_text(source).ok())
1418                            .unwrap_or("")
1419                            .to_string();
1420
1421                        if !struct_name.is_empty() {
1422                            scan_rust_struct_fields(node, &struct_name, source, instance_attr_types);
1423                        }
1424                    }
1425                    // Go: extract field types from type declarations
1426                    if kind == "type_declaration" {
1427                        scan_go_struct_fields(node, source, instance_attr_types);
1428                    }
1429                }
1430            }
1431            InitStrategy::None => {}
1432        }
1433
1434        let mut cursor = node.walk();
1435        let children: Vec<_> = node.named_children(&mut cursor).collect();
1436        for child in children.into_iter().rev() {
1437            worklist.push(child);
1438        }
1439    }
1440}
1441
1442/// Rust: extract field types from `struct Foo { conn: Connection, ... }`
1443fn scan_rust_struct_fields(
1444    node: tree_sitter::Node,
1445    struct_name: &str,
1446    source: &[u8],
1447    instance_attr_types: &mut HashMap<(String, String), String>,
1448) {
1449    let mut cursor = node.walk();
1450    for child in node.named_children(&mut cursor) {
1451        if child.kind() == "field_declaration_list" {
1452            let mut inner_cursor = child.walk();
1453            for field in child.named_children(&mut inner_cursor) {
1454                if field.kind() == "field_declaration" {
1455                    let field_name = field
1456                        .child_by_field_name("name")
1457                        .and_then(|n| n.utf8_text(source).ok())
1458                        .unwrap_or("");
1459                    let field_type = field
1460                        .child_by_field_name("type")
1461                        .map(|n| extract_base_type(n, source))
1462                        .unwrap_or_default();
1463
1464                    if !field_name.is_empty()
1465                        && !field_type.is_empty()
1466                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1467                    {
1468                        instance_attr_types.insert(
1469                            (struct_name.to_string(), field_name.to_string()),
1470                            field_type,
1471                        );
1472                    }
1473                }
1474            }
1475        }
1476    }
1477}
1478
1479/// Go: extract field types from `type Foo struct { conn Connection; ... }`
1480fn scan_go_struct_fields(
1481    node: tree_sitter::Node,
1482    source: &[u8],
1483    instance_attr_types: &mut HashMap<(String, String), String>,
1484) {
1485    let mut cursor = node.walk();
1486    for child in node.named_children(&mut cursor) {
1487        if child.kind() == "type_spec" {
1488            let struct_name = child
1489                .child_by_field_name("name")
1490                .and_then(|n| n.utf8_text(source).ok())
1491                .unwrap_or("")
1492                .to_string();
1493
1494            if struct_name.is_empty() {
1495                continue;
1496            }
1497
1498            // Look for struct_type child
1499            if let Some(type_node) = child.child_by_field_name("type") {
1500                if type_node.kind() == "struct_type" {
1501                    let mut fields_cursor = type_node.walk();
1502                    for field_list in type_node.named_children(&mut fields_cursor) {
1503                        if field_list.kind() == "field_declaration_list" {
1504                            let mut inner = field_list.walk();
1505                            for field in field_list.named_children(&mut inner) {
1506                                if field.kind() == "field_declaration" {
1507                                    // Go field: name type
1508                                    let field_name = field
1509                                        .child_by_field_name("name")
1510                                        .and_then(|n| n.utf8_text(source).ok())
1511                                        .unwrap_or("");
1512                                    let field_type = field
1513                                        .child_by_field_name("type")
1514                                        .map(|n| extract_base_type(n, source))
1515                                        .unwrap_or_default();
1516
1517                                    if !field_name.is_empty()
1518                                        && !field_type.is_empty()
1519                                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1520                                    {
1521                                        instance_attr_types.insert(
1522                                            (struct_name.clone(), field_name.to_string()),
1523                                            field_type,
1524                                        );
1525                                    }
1526                                }
1527                            }
1528                        }
1529                    }
1530                }
1531            }
1532        }
1533    }
1534}
1535
1536fn scan_class_for_init(
1537    root: tree_sitter::Node,
1538    class_name: &str,
1539    source: &[u8],
1540    instance_attr_types: &mut HashMap<(String, String), String>,
1541    init_params_map: &mut HashMap<String, Vec<String>>,
1542    attr_to_param_map: &mut HashMap<(String, String), String>,
1543    lang: &str,
1544) {
1545    let mut worklist = vec![root];
1546    while let Some(node) = worklist.pop() {
1547        let mut cursor = node.walk();
1548        for child in node.named_children(&mut cursor) {
1549            let ck = child.kind();
1550
1551            // Python __init__
1552            if ck == "function_definition" {
1553                let name = child
1554                    .child_by_field_name("name")
1555                    .and_then(|n| n.utf8_text(source).ok())
1556                    .unwrap_or("");
1557                if name == "__init__" {
1558                    let params = extract_init_params(child, source);
1559                    let ordered_params = extract_init_param_names_ordered(child, source);
1560                    init_params_map.insert(class_name.to_string(), ordered_params);
1561                    scan_init_body(child, class_name, &params, source, instance_attr_types, attr_to_param_map);
1562                }
1563            }
1564
1565            // TS constructor
1566            if ck == "method_definition" && lang == "typescript" {
1567                let name = child
1568                    .child_by_field_name("name")
1569                    .and_then(|n| n.utf8_text(source).ok())
1570                    .unwrap_or("");
1571                if name == "constructor" {
1572                    // Scan for this.attr = param patterns
1573                    scan_ts_constructor_body(child, class_name, source, instance_attr_types, init_params_map, attr_to_param_map);
1574                }
1575            }
1576
1577            // TS: typed class field declarations `private conn: Connection`
1578            if (ck == "public_field_definition" || ck == "property_declaration" || ck == "field_definition") && lang == "typescript" {
1579                let field_name = child
1580                    .child_by_field_name("name")
1581                    .and_then(|n| n.utf8_text(source).ok())
1582                    .unwrap_or("");
1583                if let Some(type_ann) = child.child_by_field_name("type") {
1584                    let type_text = extract_base_type(type_ann, source);
1585                    if !field_name.is_empty()
1586                        && !type_text.is_empty()
1587                        && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1588                    {
1589                        instance_attr_types.insert(
1590                            (class_name.to_string(), field_name.to_string()),
1591                            type_text,
1592                        );
1593                    }
1594                }
1595            }
1596
1597            if ck == "block" || ck == "class_body" || ck == "statement_block" {
1598                worklist.push(child);
1599            }
1600        }
1601    }
1602}
1603
1604/// TS: scan constructor body for `this.attr = param` patterns
1605fn scan_ts_constructor_body(
1606    node: tree_sitter::Node,
1607    class_name: &str,
1608    source: &[u8],
1609    instance_attr_types: &mut HashMap<(String, String), String>,
1610    init_params_map: &mut HashMap<String, Vec<String>>,
1611    attr_to_param_map: &mut HashMap<(String, String), String>,
1612) {
1613    // Extract constructor params
1614    let params = extract_init_params(node, source);
1615    let ordered_params = extract_init_param_names_ordered(node, source);
1616    init_params_map.insert(class_name.to_string(), ordered_params);
1617
1618    // Scan body for this.X = param
1619    scan_init_body_this(node, class_name, &params, source, instance_attr_types, attr_to_param_map);
1620}
1621
1622/// Scan constructor body for `this.attr = param` patterns (TS variant)
1623fn scan_init_body_this(
1624    root: tree_sitter::Node,
1625    class_name: &str,
1626    params: &HashMap<String, Option<String>>,
1627    source: &[u8],
1628    instance_attr_types: &mut HashMap<(String, String), String>,
1629    attr_to_param_map: &mut HashMap<(String, String), String>,
1630) {
1631    let mut worklist = vec![root];
1632    while let Some(node) = worklist.pop() {
1633        let mut cursor = node.walk();
1634        for child in node.named_children(&mut cursor) {
1635            let ck = child.kind();
1636            if ck == "expression_statement" {
1637                // Look for assignment: this.X = Y
1638                let mut inner_cursor = child.walk();
1639                for inner in child.named_children(&mut inner_cursor) {
1640                    if inner.kind() == "assignment_expression" {
1641                        if let Some(left) = inner.child_by_field_name("left") {
1642                            if left.kind() == "member_expression" {
1643                                let obj = left.child_by_field_name("object")
1644                                    .and_then(|n| n.utf8_text(source).ok())
1645                                    .unwrap_or("");
1646                                let prop = left.child_by_field_name("property")
1647                                    .and_then(|n| n.utf8_text(source).ok())
1648                                    .unwrap_or("");
1649                                if obj == "this" && !prop.is_empty() {
1650                                    if let Some(right) = inner.child_by_field_name("right") {
1651                                        if right.kind() == "identifier" {
1652                                            let rhs_name = right.utf8_text(source).unwrap_or("");
1653                                            if params.contains_key(rhs_name) {
1654                                                attr_to_param_map.insert(
1655                                                    (class_name.to_string(), prop.to_string()),
1656                                                    rhs_name.to_string(),
1657                                                );
1658                                                if let Some(Some(type_hint)) = params.get(rhs_name) {
1659                                                    instance_attr_types.insert(
1660                                                        (class_name.to_string(), prop.to_string()),
1661                                                        type_hint.clone(),
1662                                                    );
1663                                                }
1664                                            }
1665                                        }
1666                                        if right.kind() == "new_expression" {
1667                                            if let Some(ctor) = right.child_by_field_name("constructor") {
1668                                                let name = ctor.utf8_text(source).unwrap_or("");
1669                                                if !name.is_empty() {
1670                                                    instance_attr_types.insert(
1671                                                        (class_name.to_string(), prop.to_string()),
1672                                                        name.to_string(),
1673                                                    );
1674                                                }
1675                                            }
1676                                        }
1677                                    }
1678                                }
1679                            }
1680                        }
1681                    }
1682                }
1683            }
1684            if ck == "statement_block" || ck == "block" {
1685                worklist.push(child);
1686            }
1687        }
1688    }
1689}
1690
1691/// Extract __init__ parameter names in order (excluding self).
1692fn extract_init_param_names_ordered(func_node: tree_sitter::Node, source: &[u8]) -> Vec<String> {
1693    let mut names = Vec::new();
1694    if let Some(params_node) = func_node.child_by_field_name("parameters") {
1695        let mut cursor = params_node.walk();
1696        for child in params_node.named_children(&mut cursor) {
1697            let param_name = if child.kind() == "identifier" {
1698                child.utf8_text(source).unwrap_or("").to_string()
1699            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
1700                child.child_by_field_name("name")
1701                    .or_else(|| child.named_child(0))
1702                    .and_then(|n| n.utf8_text(source).ok())
1703                    .unwrap_or("")
1704                    .to_string()
1705            } else {
1706                continue;
1707            };
1708            if param_name != "self" && param_name != "cls" && !param_name.is_empty() {
1709                names.push(param_name);
1710            }
1711        }
1712    }
1713    names
1714}
1715
1716fn extract_init_params(func_node: tree_sitter::Node, source: &[u8]) -> HashMap<String, Option<String>> {
1717    let mut params = HashMap::new();
1718    if let Some(params_node) = func_node.child_by_field_name("parameters") {
1719        let mut cursor = params_node.walk();
1720        for child in params_node.named_children(&mut cursor) {
1721            let param_name = if child.kind() == "identifier" {
1722                child.utf8_text(source).unwrap_or("").to_string()
1723            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
1724                child.child_by_field_name("name")
1725                    .or_else(|| child.named_child(0))
1726                    .and_then(|n| n.utf8_text(source).ok())
1727                    .unwrap_or("")
1728                    .to_string()
1729            } else {
1730                continue;
1731            };
1732            if param_name != "self" && param_name != "cls" {
1733                // Check for type annotation
1734                let type_hint = child.child_by_field_name("type")
1735                    .and_then(|n| n.utf8_text(source).ok())
1736                    .map(|s| s.to_string());
1737                params.insert(param_name, type_hint);
1738            }
1739        }
1740    }
1741    params
1742}
1743
1744fn scan_init_body(
1745    root: tree_sitter::Node,
1746    class_name: &str,
1747    params: &HashMap<String, Option<String>>,
1748    source: &[u8],
1749    instance_attr_types: &mut HashMap<(String, String), String>,
1750    attr_to_param_map: &mut HashMap<(String, String), String>,
1751) {
1752    let mut worklist = vec![root];
1753    while let Some(node) = worklist.pop() {
1754        let mut cursor = node.walk();
1755        for child in node.named_children(&mut cursor) {
1756            if child.kind() == "expression_statement" || child.kind() == "assignment" {
1757                let assign = if child.kind() == "assignment" {
1758                    child
1759                } else {
1760                    let mut inner_cursor = child.walk();
1761                    let children: Vec<_> = child.named_children(&mut inner_cursor).collect();
1762                    match children.into_iter().find(|c| c.kind() == "assignment") {
1763                        Some(a) => a,
1764                        None => continue,
1765                    }
1766                };
1767
1768                if let Some(left) = assign.child_by_field_name("left") {
1769                    if left.kind() == "attribute" {
1770                        let obj = left.child_by_field_name("object")
1771                            .and_then(|n| n.utf8_text(source).ok())
1772                            .unwrap_or("");
1773                        let attr = left.child_by_field_name("attribute")
1774                            .and_then(|n| n.utf8_text(source).ok())
1775                            .unwrap_or("");
1776
1777                        if obj == "self" && !attr.is_empty() {
1778                            if let Some(right) = assign.child_by_field_name("right") {
1779                                if right.kind() == "identifier" {
1780                                    let rhs_name = right.utf8_text(source).unwrap_or("");
1781                                    // Record attr -> param mapping for later inference
1782                                    if params.contains_key(rhs_name) {
1783                                        attr_to_param_map.insert(
1784                                            (class_name.to_string(), attr.to_string()),
1785                                            rhs_name.to_string(),
1786                                        );
1787                                    }
1788                                    // If param has type hint, directly set the type
1789                                    if let Some(Some(type_hint)) = params.get(rhs_name) {
1790                                        instance_attr_types.insert(
1791                                            (class_name.to_string(), attr.to_string()),
1792                                            type_hint.clone(),
1793                                        );
1794                                    }
1795                                }
1796                                if right.kind() == "call" {
1797                                    if let Some(func) = right.child_by_field_name("function") {
1798                                        if func.kind() == "identifier" {
1799                                            let fname = func.utf8_text(source).unwrap_or("");
1800                                            if fname.chars().next().map_or(false, |c| c.is_uppercase()) {
1801                                                instance_attr_types.insert(
1802                                                    (class_name.to_string(), attr.to_string()),
1803                                                    fname.to_string(),
1804                                                );
1805                                            }
1806                                        }
1807                                    }
1808                                }
1809                            }
1810                        }
1811                    }
1812                }
1813            }
1814            if child.kind() == "block" {
1815                worklist.push(child);
1816            }
1817        }
1818    }
1819}
1820
1821/// Infer constructor parameter types by analyzing call sites across all files.
1822/// For `Transaction(get_connection())`, we know get_connection() returns Connection,
1823/// so Transaction.__init__'s conn param has type Connection,
1824/// and self.conn in Transaction has type Connection.
1825fn infer_constructor_param_types(
1826    parsed_files: &[(String, String, tree_sitter::Tree)],
1827    return_type_map: &HashMap<String, String>,
1828    init_params: &HashMap<String, Vec<String>>,
1829    attr_to_param: &HashMap<(String, String), String>,
1830    _symbol_table: &HashMap<String, Vec<String>>,
1831    entity_map: &HashMap<String, EntityInfo>,
1832    instance_attr_types: &mut HashMap<(String, String), String>,
1833) {
1834    // Build func_name -> return_type lookup for quick access
1835    let mut func_name_returns: HashMap<String, String> = HashMap::new();
1836    for (eid, ret_type) in return_type_map {
1837        if let Some(info) = entity_map.get(eid) {
1838            func_name_returns.insert(info.name.clone(), ret_type.clone());
1839        }
1840    }
1841
1842    // Scan all files for constructor call sites: ClassName(arg1, arg2, ...)
1843    // Parallelized: each file produces local results, then merged.
1844    let local_results: Vec<HashMap<(String, String), String>> = maybe_par_iter!(parsed_files)
1845        .map(|(_file_path, content, tree)| {
1846            let source = content.as_bytes();
1847            let mut local_attr_types: HashMap<(String, String), String> = HashMap::new();
1848            scan_constructor_calls(
1849                tree.root_node(),
1850                source,
1851                &func_name_returns,
1852                init_params,
1853                attr_to_param,
1854                &mut local_attr_types,
1855            );
1856            local_attr_types
1857        })
1858        .collect();
1859
1860    for local in local_results {
1861        for (key, val) in local {
1862            instance_attr_types.entry(key).or_insert(val);
1863        }
1864    }
1865}
1866
1867fn scan_constructor_calls(
1868    root: tree_sitter::Node,
1869    source: &[u8],
1870    func_name_returns: &HashMap<String, String>,
1871    init_params: &HashMap<String, Vec<String>>,
1872    attr_to_param: &HashMap<(String, String), String>,
1873    instance_attr_types: &mut HashMap<(String, String), String>,
1874) {
1875    let mut worklist = vec![root];
1876    while let Some(node) = worklist.pop() {
1877        let kind = node.kind();
1878
1879        if kind == "call" {
1880            if let Some(func) = node.child_by_field_name("function") {
1881                if func.kind() == "identifier" {
1882                    let class_name = func.utf8_text(source).unwrap_or("");
1883                    // Only process uppercase names (constructor calls)
1884                    if class_name.chars().next().map_or(false, |c| c.is_uppercase()) {
1885                        if let Some(param_names) = init_params.get(class_name) {
1886                            // Extract argument types
1887                            if let Some(args_node) = node.child_by_field_name("arguments") {
1888                                let mut arg_idx = 0;
1889                                let mut args_cursor = args_node.walk();
1890                                for arg in args_node.named_children(&mut args_cursor) {
1891                                    if arg_idx >= param_names.len() {
1892                                        break;
1893                                    }
1894                                    let param_name = &param_names[arg_idx];
1895
1896                                    // Try to infer the argument's type
1897                                    let arg_type = infer_expr_type(arg, source, func_name_returns);
1898
1899                                    if let Some(at) = arg_type {
1900                                        // Check if any self.attr maps to this param
1901                                        for ((cn, attr), pn) in attr_to_param.iter() {
1902                                            if cn == class_name && pn == param_name {
1903                                                instance_attr_types
1904                                                    .entry((cn.clone(), attr.clone()))
1905                                                    .or_insert(at.clone());
1906                                            }
1907                                        }
1908                                    }
1909
1910                                    arg_idx += 1;
1911                                }
1912                            }
1913                        }
1914                    }
1915                }
1916            }
1917        }
1918
1919        let mut cursor = node.walk();
1920        let children: Vec<_> = node.named_children(&mut cursor).collect();
1921        for child in children.into_iter().rev() {
1922            worklist.push(child);
1923        }
1924    }
1925}
1926
1927/// Infer the type of an expression node.
1928fn infer_expr_type(
1929    node: tree_sitter::Node,
1930    source: &[u8],
1931    func_name_returns: &HashMap<String, String>,
1932) -> Option<String> {
1933    match node.kind() {
1934        "call" => {
1935            if let Some(func) = node.child_by_field_name("function") {
1936                if func.kind() == "identifier" {
1937                    let name = func.utf8_text(source).unwrap_or("");
1938                    // Constructor call: Foo() -> type is Foo
1939                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1940                        return Some(name.to_string());
1941                    }
1942                    // Function call with known return type
1943                    if let Some(ret) = func_name_returns.get(name) {
1944                        return Some(ret.clone());
1945                    }
1946                }
1947            }
1948            None
1949        }
1950        "identifier" => {
1951            // Could be a variable, but we don't have scope info here
1952            None
1953        }
1954        _ => None,
1955    }
1956}
1957
1958/// Resolve pending call types using the return type map.
1959/// For scopes with `x = func()` where func has a known return type, bind x to that type.
1960fn inject_return_type_bindings(
1961    _entity_inner_scope: &HashMap<String, usize>,
1962    scopes: &mut Vec<Scope>,
1963    return_type_map: &HashMap<String, String>,
1964    import_table: &HashMap<(String, String), String>,
1965    file_path: &str,
1966    entity_map: &HashMap<String, EntityInfo>,
1967) {
1968    // Build function name -> return type lookup
1969    let mut func_name_return_types: HashMap<String, String> = HashMap::new();
1970    for (eid, ret_type) in return_type_map {
1971        if let Some(info) = entity_map.get(eid) {
1972            func_name_return_types.insert(info.name.clone(), ret_type.clone());
1973        }
1974    }
1975
1976    // Also resolve through imports: if `get_connection` is imported and has a known return type
1977    for ((fp, local_name), target_id) in import_table {
1978        if fp == file_path {
1979            if let Some(ret_type) = return_type_map.get(target_id) {
1980                func_name_return_types.insert(local_name.clone(), ret_type.clone());
1981            }
1982        }
1983    }
1984
1985    // Resolve pending call types in all scopes
1986    for scope in scopes.iter_mut() {
1987        let resolved: Vec<(String, String)> = scope
1988            .pending_call_types
1989            .iter()
1990            .filter_map(|(var_name, func_name)| {
1991                func_name_return_types
1992                    .get(func_name)
1993                    .map(|ret_type| (var_name.clone(), ret_type.clone()))
1994            })
1995            .collect();
1996
1997        for (var_name, ret_type) in resolved {
1998            scope.types.insert(var_name, ret_type);
1999        }
2000    }
2001}
2002
2003/// Extract import statements from the AST.
2004fn extract_imports_from_ast(
2005    root: tree_sitter::Node,
2006    file_path: &str,
2007    source: &[u8],
2008    symbol_table: &HashMap<String, Vec<String>>,
2009    entity_map: &HashMap<String, EntityInfo>,
2010    import_table: &mut HashMap<(String, String), String>,
2011    scopes: &mut Vec<Scope>,
2012    config: &ScopeResolveConfig,
2013    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2014) {
2015    let mut worklist = vec![root];
2016    while let Some(node) = worklist.pop() {
2017        let mut cursor = node.walk();
2018        for child in node.named_children(&mut cursor) {
2019            let ck = child.kind();
2020            let handled = match ck {
2021                "import_from_statement" => {
2022                    extract_python_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2023                    true
2024                }
2025                "import_statement" if config.self_keywords.contains(&"self") && config.self_keywords.contains(&"cls") => {
2026                    // Python: `import mod` or `import mod as m`
2027                    extract_python_module_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2028                    true
2029                }
2030                "import_statement" if !config.self_keywords.contains(&"cls") => {
2031                    // TS import_statement (not Python - Python uses import_from_statement)
2032                    extract_ts_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2033                    true
2034                }
2035                "use_declaration" => {
2036                    extract_rust_use(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2037                    true
2038                }
2039                "import_declaration" => {
2040                    extract_go_import(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2041                    true
2042                }
2043                _ => false,
2044            };
2045            if !handled {
2046                worklist.push(child);
2047            }
2048        }
2049    }
2050}
2051
2052/// TS: `import { Foo, Bar } from './module'` or `import Foo from './module'`
2053fn extract_ts_import(
2054    node: tree_sitter::Node,
2055    file_path: &str,
2056    source: &[u8],
2057    symbol_table: &HashMap<String, Vec<String>>,
2058    entity_map: &HashMap<String, EntityInfo>,
2059    import_table: &mut HashMap<(String, String), String>,
2060    scopes: &mut Vec<Scope>,
2061) {
2062    // Extract the source module from the `from '...'` clause
2063    let source_path = node
2064        .child_by_field_name("source")
2065        .and_then(|n| n.utf8_text(source).ok())
2066        .unwrap_or("")
2067        .trim_matches(|c: char| c == '\'' || c == '"');
2068
2069    let source_module = source_path
2070        .rsplit('/')
2071        .next()
2072        .unwrap_or(source_path);
2073    // Strip extensions
2074    let source_module = source_module
2075        .strip_suffix(".ts").or_else(|| source_module.strip_suffix(".js"))
2076        .or_else(|| source_module.strip_suffix(".tsx")).or_else(|| source_module.strip_suffix(".jsx"))
2077        .unwrap_or(source_module);
2078
2079    if source_module.is_empty() {
2080        return;
2081    }
2082
2083    // Walk children to find import clause
2084    let mut cursor = node.walk();
2085    for child in node.named_children(&mut cursor) {
2086        if child.kind() == "import_clause" {
2087            let mut clause_cursor = child.walk();
2088            for clause_child in child.named_children(&mut clause_cursor) {
2089                if clause_child.kind() == "named_imports" {
2090                    // { Foo, Bar as Baz }
2091                    let mut imports_cursor = clause_child.walk();
2092                    for spec in clause_child.named_children(&mut imports_cursor) {
2093                        if spec.kind() == "import_specifier" {
2094                            let original = spec
2095                                .child_by_field_name("name")
2096                                .and_then(|n| n.utf8_text(source).ok())
2097                                .unwrap_or("");
2098                            let local = spec
2099                                .child_by_field_name("alias")
2100                                .and_then(|n| n.utf8_text(source).ok())
2101                                .unwrap_or(original);
2102
2103                            if !original.is_empty() {
2104                                resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2105                            }
2106                        }
2107                    }
2108                } else if clause_child.kind() == "namespace_import" {
2109                    // import * as m from './module'
2110                    // Register all entities from source module so m.foo() resolves
2111                    let mut ns_cursor = clause_child.walk();
2112                    let alias = clause_child
2113                        .child_by_field_name("alias")
2114                        .or_else(|| {
2115                            clause_child.named_children(&mut ns_cursor)
2116                                .find(|c| c.kind() == "identifier")
2117                        })
2118                        .and_then(|n| n.utf8_text(source).ok())
2119                        .unwrap_or("");
2120                    if !alias.is_empty() {
2121                        register_namespace_import(alias, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2122                    }
2123                } else if clause_child.kind() == "identifier" {
2124                    // Default import: import Foo from './module'
2125                    let name = clause_child.utf8_text(source).unwrap_or("");
2126                    if !name.is_empty() {
2127                        resolve_import_name(name, name, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2128                    }
2129                }
2130            }
2131        }
2132    }
2133}
2134
2135/// Rust: `use crate::module::Name;` or `use crate::module::{A, B};`
2136/// Parse from the text of the use_declaration for reliability.
2137fn extract_rust_use(
2138    node: tree_sitter::Node,
2139    file_path: &str,
2140    source: &[u8],
2141    symbol_table: &HashMap<String, Vec<String>>,
2142    entity_map: &HashMap<String, EntityInfo>,
2143    import_table: &mut HashMap<(String, String), String>,
2144    scopes: &mut Vec<Scope>,
2145) {
2146    let text = node.utf8_text(source).unwrap_or("").trim().to_string();
2147    // Strip `use ` prefix and trailing `;`
2148    let text = text.strip_prefix("use ").unwrap_or(&text);
2149    let text = text.strip_prefix("pub use ").unwrap_or(text);
2150    let text = text.trim_end_matches(';').trim();
2151
2152    // Strip crate/super/self prefix
2153    let text = text
2154        .strip_prefix("crate::")
2155        .or_else(|| text.strip_prefix("super::"))
2156        .or_else(|| text.strip_prefix("self::"))
2157        .unwrap_or(text);
2158
2159    // Check for grouped import: module::{A, B, C}
2160    if let Some(brace_pos) = text.find("::{") {
2161        let module_path = &text[..brace_pos];
2162        let source_module = module_path.rsplit("::").next().unwrap_or(module_path);
2163
2164        let names_part = &text[brace_pos + 3..];
2165        let names_part = names_part.trim_end_matches('}');
2166
2167        for name_part in names_part.split(',') {
2168            let name_part = name_part.trim();
2169            if name_part.is_empty() {
2170                continue;
2171            }
2172            let (original, local) = if let Some(pos) = name_part.find(" as ") {
2173                (name_part[..pos].trim(), name_part[pos + 4..].trim())
2174            } else {
2175                (name_part, name_part)
2176            };
2177            if !original.is_empty() {
2178                resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2179            }
2180        }
2181    } else {
2182        // Simple import: module::Name
2183        let parts: Vec<&str> = text.split("::").collect();
2184        if parts.is_empty() {
2185            return;
2186        }
2187        let imported_name = parts.last().unwrap().trim();
2188        let (original, local) = if let Some(pos) = imported_name.find(" as ") {
2189            (&imported_name[..pos], imported_name[pos + 4..].trim())
2190        } else {
2191            (imported_name, imported_name)
2192        };
2193        let source_module = if parts.len() >= 2 {
2194            parts[parts.len() - 2]
2195        } else {
2196            parts[0]
2197        };
2198        if !original.is_empty() && !source_module.is_empty() {
2199            resolve_import_name(original, local, source_module, file_path, symbol_table, entity_map, import_table, scopes);
2200        }
2201    }
2202}
2203
2204/// Go: `import ("module/path")` - maps package names to entities
2205fn extract_go_import(
2206    node: tree_sitter::Node,
2207    file_path: &str,
2208    source: &[u8],
2209    symbol_table: &HashMap<String, Vec<String>>,
2210    entity_map: &HashMap<String, EntityInfo>,
2211    import_table: &mut HashMap<(String, String), String>,
2212    scopes: &mut Vec<Scope>,
2213    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2214) {
2215    let mut cursor = node.walk();
2216    for child in node.named_children(&mut cursor) {
2217        if child.kind() == "import_spec" || child.kind() == "import_spec_list" {
2218            extract_go_import_specs(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2219        } else if child.kind() == "interpreted_string_literal" || child.kind() == "raw_string_literal" {
2220            let path = child.utf8_text(source).unwrap_or("")
2221                .trim_matches('"').trim_matches('`');
2222            let pkg_name = path.rsplit('/').next().unwrap_or(path);
2223            register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2224        }
2225    }
2226}
2227
2228fn extract_go_import_specs(
2229    root: tree_sitter::Node,
2230    file_path: &str,
2231    source: &[u8],
2232    symbol_table: &HashMap<String, Vec<String>>,
2233    entity_map: &HashMap<String, EntityInfo>,
2234    import_table: &mut HashMap<(String, String), String>,
2235    scopes: &mut Vec<Scope>,
2236    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2237) {
2238    let mut worklist = vec![root];
2239    while let Some(node) = worklist.pop() {
2240        let mut cursor = node.walk();
2241        for child in node.named_children(&mut cursor) {
2242            if child.kind() == "import_spec" {
2243                let path_node = child.child_by_field_name("path")
2244                    .or_else(|| child.named_child(0));
2245                if let Some(pn) = path_node {
2246                    let path = pn.utf8_text(source).unwrap_or("")
2247                        .trim_matches('"').trim_matches('`');
2248                    let pkg_name = path.rsplit('/').next().unwrap_or(path);
2249                    register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2250                }
2251            } else {
2252                worklist.push(child);
2253            }
2254        }
2255    }
2256}
2257
2258fn register_go_package_imports(
2259    pkg_name: &str,
2260    file_path: &str,
2261    _symbol_table: &HashMap<String, Vec<String>>,
2262    _entity_map: &HashMap<String, EntityInfo>,
2263    import_table: &mut HashMap<(String, String), String>,
2264    scopes: &mut Vec<Scope>,
2265    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2266) {
2267    // Use pre-built package index for O(1) lookup instead of O(symbol_table) scan
2268    if let Some(entries) = go_pkg_index.get(pkg_name) {
2269        for (name, target_id) in entries {
2270            import_table.insert(
2271                (file_path.to_string(), name.clone()),
2272                target_id.clone(),
2273            );
2274            if !scopes.is_empty() {
2275                scopes[0].defs.insert(name.clone(), target_id.clone());
2276            }
2277        }
2278    }
2279}
2280
2281/// Shared helper: resolve an imported name against the symbol table
2282fn resolve_import_name(
2283    original_name: &str,
2284    local_name: &str,
2285    source_module: &str,
2286    file_path: &str,
2287    symbol_table: &HashMap<String, Vec<String>>,
2288    entity_map: &HashMap<String, EntityInfo>,
2289    import_table: &mut HashMap<(String, String), String>,
2290    scopes: &mut Vec<Scope>,
2291) {
2292    if let Some(target_ids) = symbol_table.get(original_name) {
2293        let target = target_ids.iter().find(|id| {
2294            entity_map.get(*id).map_or(false, |e| {
2295                let stem = e.file_path.rsplit('/').next().unwrap_or(&e.file_path);
2296                let stem = stem
2297                    .strip_suffix(".py")
2298                    .or_else(|| stem.strip_suffix(".rs"))
2299                    .or_else(|| stem.strip_suffix(".ts"))
2300                    .or_else(|| stem.strip_suffix(".tsx"))
2301                    .or_else(|| stem.strip_suffix(".js"))
2302                    .or_else(|| stem.strip_suffix(".jsx"))
2303                    .or_else(|| stem.strip_suffix(".go"))
2304                    .unwrap_or(stem);
2305                stem == source_module
2306            })
2307        });
2308
2309        if let Some(target_id) = target {
2310            import_table.insert(
2311                (file_path.to_string(), local_name.to_string()),
2312                target_id.clone(),
2313            );
2314            if !scopes.is_empty() {
2315                scopes[0]
2316                    .defs
2317                    .insert(local_name.to_string(), target_id.clone());
2318            }
2319        }
2320    }
2321}
2322
2323/// Register all entities from a source module under a namespace alias.
2324/// For `import * as m from './module'`, all entities from the module
2325/// are registered so that `m.foo()` resolves via the method call path.
2326fn register_namespace_import(
2327    alias: &str,
2328    source_module: &str,
2329    file_path: &str,
2330    symbol_table: &HashMap<String, Vec<String>>,
2331    entity_map: &HashMap<String, EntityInfo>,
2332    import_table: &mut HashMap<(String, String), String>,
2333    scopes: &mut Vec<Scope>,
2334) {
2335    // Find all entities whose file matches the source_module stem
2336    for (name, target_ids) in symbol_table {
2337        for target_id in target_ids {
2338            if let Some(info) = entity_map.get(target_id) {
2339                let stem = info.file_path.rsplit('/').next().unwrap_or(&info.file_path);
2340                let stem = stem
2341                    .strip_suffix(".ts")
2342                    .or_else(|| stem.strip_suffix(".js"))
2343                    .or_else(|| stem.strip_suffix(".tsx"))
2344                    .or_else(|| stem.strip_suffix(".jsx"))
2345                    .or_else(|| stem.strip_suffix(".py"))
2346                    .unwrap_or(stem);
2347                if stem == source_module && info.parent_id.is_none() {
2348                    // Register entity_name under the alias for method-call resolution
2349                    import_table.insert(
2350                        (file_path.to_string(), name.clone()),
2351                        target_id.clone(),
2352                    );
2353                    if !scopes.is_empty() {
2354                        scopes[0].defs.insert(name.clone(), target_id.clone());
2355                    }
2356                }
2357            }
2358        }
2359    }
2360    // Also register the alias itself so that type tracking for `alias.method()` works:
2361    // the resolve_ref MethodCall path checks import_table for the receiver.
2362    // We don't have a single entity for the module, so this is handled by the
2363    // Go-style package-qualified call fallback (checking method name in import_table).
2364    let _ = (alias, file_path); // used above
2365}
2366
2367fn extract_python_import(
2368    node: tree_sitter::Node,
2369    file_path: &str,
2370    source: &[u8],
2371    symbol_table: &HashMap<String, Vec<String>>,
2372    entity_map: &HashMap<String, EntityInfo>,
2373    import_table: &mut HashMap<(String, String), String>,
2374    scopes: &mut Vec<Scope>,
2375) {
2376    // import_from_statement has:
2377    //   module_name (dotted_name or relative_import)
2378    //   name fields (imported names)
2379    let module_node = node.child_by_field_name("module_name");
2380    let module_name = module_node
2381        .and_then(|n| n.utf8_text(source).ok())
2382        .unwrap_or("");
2383
2384    let source_module = module_name
2385        .trim_start_matches('.')
2386        .rsplit('.')
2387        .next()
2388        .unwrap_or(module_name.trim_start_matches('.'));
2389
2390    // Walk children to find imported names
2391    let mut cursor = node.walk();
2392    for child in node.named_children(&mut cursor) {
2393        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
2394            let (original, local) = if child.kind() == "aliased_import" {
2395                let orig = child
2396                    .child_by_field_name("name")
2397                    .and_then(|n| n.utf8_text(source).ok())
2398                    .unwrap_or("");
2399                let alias = child
2400                    .child_by_field_name("alias")
2401                    .and_then(|n| n.utf8_text(source).ok())
2402                    .unwrap_or(orig);
2403                (orig, alias)
2404            } else {
2405                let name = child.utf8_text(source).unwrap_or("");
2406                (name, name)
2407            };
2408
2409            if original.is_empty() {
2410                continue;
2411            }
2412
2413            // Resolve against symbol table, preferring entities from the source module
2414            if let Some(target_ids) = symbol_table.get(original) {
2415                let target = target_ids.iter().find(|id| {
2416                    entity_map.get(*id).map_or(false, |e| {
2417                        let stem = e.file_path.rsplit('/').next().unwrap_or(&e.file_path);
2418                        let stem = stem
2419                            .strip_suffix(".py")
2420                            .or_else(|| stem.strip_suffix(".rs"))
2421                            .or_else(|| stem.strip_suffix(".ts"))
2422                            .or_else(|| stem.strip_suffix(".js"))
2423                            .unwrap_or(stem);
2424                        stem == source_module
2425                    })
2426                });
2427
2428                if let Some(target_id) = target {
2429                    import_table.insert(
2430                        (file_path.to_string(), local.to_string()),
2431                        target_id.clone(),
2432                    );
2433                    // Also add to module scope definitions
2434                    if !scopes.is_empty() {
2435                        scopes[0]
2436                            .defs
2437                            .insert(local.to_string(), target_id.clone());
2438                    }
2439                }
2440            }
2441        }
2442    }
2443}
2444
2445/// Python: `import mod` or `import mod as m` — registers all entities from
2446/// the module so that `m.foo()` resolves via the method-call path.
2447fn extract_python_module_import(
2448    node: tree_sitter::Node,
2449    file_path: &str,
2450    source: &[u8],
2451    symbol_table: &HashMap<String, Vec<String>>,
2452    entity_map: &HashMap<String, EntityInfo>,
2453    import_table: &mut HashMap<(String, String), String>,
2454    scopes: &mut Vec<Scope>,
2455) {
2456    let mut cursor = node.walk();
2457    for child in node.named_children(&mut cursor) {
2458        let (module_name, _alias) = match child.kind() {
2459            "dotted_name" => {
2460                let name = child.utf8_text(source).unwrap_or("");
2461                (name, name)
2462            }
2463            "aliased_import" => {
2464                let orig = child
2465                    .child_by_field_name("name")
2466                    .and_then(|n| n.utf8_text(source).ok())
2467                    .unwrap_or("");
2468                let alias = child
2469                    .child_by_field_name("alias")
2470                    .and_then(|n| n.utf8_text(source).ok())
2471                    .unwrap_or(orig);
2472                (orig, alias)
2473            }
2474            _ => continue,
2475        };
2476
2477        if module_name.is_empty() {
2478            continue;
2479        }
2480
2481        // The module stem is the last component of the dotted name
2482        let source_module = module_name.rsplit('.').next().unwrap_or(module_name);
2483
2484        // Register all entities from the source module
2485        register_namespace_import(
2486            _alias,
2487            source_module,
2488            file_path,
2489            symbol_table,
2490            entity_map,
2491            import_table,
2492            scopes,
2493        );
2494    }
2495}
2496
2497/// Collect ALL AST references in a file with a single tree walk.
2498/// Each ref records its row so callers can bucket refs into entities by line range.
2499fn collect_all_file_refs(
2500    root: tree_sitter::Node,
2501    source: &[u8],
2502    config: &ScopeResolveConfig,
2503) -> Vec<AstRef> {
2504    let mut refs = Vec::new();
2505    let mut worklist = vec![root];
2506    while let Some(node) = worklist.pop() {
2507        let node_row = node.start_position().row;
2508        let kind = node.kind();
2509
2510        // Call nodes (e.g. "call", "call_expression", "method_invocation")
2511        if config.call_nodes.contains(&kind) {
2512            match &config.call_style {
2513                CallNodeStyle::FunctionField(field) => {
2514                    if let Some(func) = node.child_by_field_name(field) {
2515                        // Pass empty entity_name — self-ref filtering is done at resolution time
2516                        extract_call_ref(func, "", "", source, &mut refs, config, node_row);
2517                    }
2518                }
2519                CallNodeStyle::DirectMethod { object_field, method_field } => {
2520                    let method_name = node.child_by_field_name(method_field)
2521                        .and_then(|n| n.utf8_text(source).ok())
2522                        .unwrap_or("");
2523                    if !method_name.is_empty() && !is_builtin(method_name, config) {
2524                        if let Some(obj_node) = node.child_by_field_name(object_field) {
2525                            let receiver = obj_node.utf8_text(source).unwrap_or("").to_string();
2526                            let receiver = receiver.trim_end_matches('.').to_string();
2527                            refs.push(AstRef {
2528                                kind: AstRefKind::MethodCall { receiver, method: method_name.to_string() },
2529                                row: node_row,
2530                            });
2531                        } else {
2532                            refs.push(AstRef {
2533                                kind: AstRefKind::Call(method_name.to_string()),
2534                                row: node_row,
2535                            });
2536                        }
2537                    }
2538                }
2539            }
2540            let mut cursor = node.walk();
2541            let children: Vec<_> = node.named_children(&mut cursor).collect();
2542            for child in children.into_iter().rev() {
2543                worklist.push(child);
2544            }
2545            continue;
2546        }
2547
2548        // Macro invocations (Rust: macro_invocation, macro name in "macro" field)
2549        if kind == "macro_invocation" {
2550            if let Some(macro_node) = node.child_by_field_name("macro") {
2551                let macro_name = macro_node.utf8_text(source).unwrap_or("");
2552                if !macro_name.is_empty() && !is_builtin(macro_name, config) {
2553                    refs.push(AstRef {
2554                        kind: AstRefKind::Call(macro_name.to_string()),
2555                        row: node_row,
2556                    });
2557                }
2558            }
2559            let mut cursor = node.walk();
2560            let children: Vec<_> = node.named_children(&mut cursor).collect();
2561            for child in children.into_iter().rev() {
2562                worklist.push(child);
2563            }
2564            continue;
2565        }
2566
2567        // New expression nodes (e.g. "new_expression", "object_creation_expression")
2568        if config.new_expr_nodes.contains(&kind) {
2569            if let Some(type_node) = node.child_by_field_name(config.new_expr_type_field) {
2570                let name = type_node.utf8_text(source).unwrap_or("");
2571                let name = name.rsplit('.').next().unwrap_or(name);
2572                if !name.is_empty() && !is_builtin(name, config) {
2573                    refs.push(AstRef {
2574                        kind: AstRefKind::Call(name.to_string()),
2575                        row: node_row,
2576                    });
2577                }
2578            }
2579            let mut cursor = node.walk();
2580            let children: Vec<_> = node.named_children(&mut cursor).collect();
2581            for child in children.into_iter().rev() {
2582                worklist.push(child);
2583            }
2584            continue;
2585        }
2586
2587        // Composite literal nodes (e.g. Go "composite_literal")
2588        if config.composite_literal_nodes.contains(&kind) {
2589            if let Some(type_node) = node.child_by_field_name("type") {
2590                let name = type_node.utf8_text(source).unwrap_or("");
2591                if name.chars().next().map_or(false, |c| c.is_uppercase())
2592                    && !is_builtin(name, config)
2593                {
2594                    refs.push(AstRef {
2595                        kind: AstRefKind::Call(name.to_string()),
2596                        row: node_row,
2597                    });
2598                }
2599            }
2600        }
2601
2602        // Recurse into children
2603        let mut cursor = node.walk();
2604        let children: Vec<_> = node.named_children(&mut cursor).collect();
2605        for child in children.into_iter().rev() {
2606            worklist.push(child);
2607        }
2608    }
2609    refs
2610}
2611
2612/// Extract a call reference from a function/callee node (shared across languages)
2613fn extract_call_ref(
2614    func: tree_sitter::Node,
2615    _entity_id: &str,
2616    entity_name: &str,
2617    source: &[u8],
2618    refs: &mut Vec<AstRef>,
2619    config: &ScopeResolveConfig,
2620    row: usize,
2621) {
2622    let func_kind = func.kind();
2623
2624    if func_kind == "identifier" {
2625        let name = func.utf8_text(source).unwrap_or("");
2626        if !name.is_empty() && name != entity_name && !is_builtin(name, config) {
2627            refs.push(AstRef {
2628                kind: AstRefKind::Call(name.to_string()),
2629                row,
2630            });
2631        }
2632        return;
2633    }
2634
2635    // Check config member_access patterns
2636    for ma in config.member_access {
2637        if func_kind == ma.node_kind {
2638            extract_member_call_ref(func, ma.object_field, ma.property_field, source, refs, row);
2639            return;
2640        }
2641    }
2642
2643    // Scoped call nodes (e.g. Rust "scoped_identifier" for Type::method)
2644    if config.scoped_call_nodes.contains(&func_kind) {
2645        let text = func.utf8_text(source).unwrap_or("");
2646        let parts: Vec<&str> = text.split("::").collect();
2647        if parts.len() >= 2 {
2648            // Handle super:: and self:: prefixed paths by emitting a call
2649            // to the final name so scope chain resolution can find it.
2650            let is_path_prefix =
2651                parts[0] == "super" || parts[0] == "self" || parts[0] == "crate";
2652            let method_name = parts[parts.len() - 1];
2653            if is_path_prefix {
2654                if !method_name.is_empty() && !is_builtin(method_name, config) {
2655                    refs.push(AstRef {
2656                        kind: AstRefKind::Call(method_name.to_string()),
2657                        row,
2658                    });
2659                }
2660            } else {
2661                let type_name = parts[parts.len() - 2];
2662                if !type_name.is_empty() && !method_name.is_empty() {
2663                    refs.push(AstRef {
2664                        kind: AstRefKind::Call(method_name.to_string()),
2665                        row,
2666                    });
2667                    if type_name.chars().next().map_or(false, |c| c.is_uppercase())
2668                        && !is_builtin(type_name, config)
2669                    {
2670                        refs.push(AstRef {
2671                            kind: AstRefKind::Call(type_name.to_string()),
2672                            row,
2673                        });
2674                    }
2675                }
2676            }
2677        }
2678    }
2679}
2680
2681/// Extract a member/method call from a node with object+property fields
2682fn extract_member_call_ref(
2683    node: tree_sitter::Node,
2684    object_field: &str,
2685    attr_field: &str,
2686    source: &[u8],
2687    refs: &mut Vec<AstRef>,
2688    row: usize,
2689) {
2690    let obj = node
2691        .child_by_field_name(object_field)
2692        .and_then(|n| n.utf8_text(source).ok())
2693        .unwrap_or("");
2694    let attr = node
2695        .child_by_field_name(attr_field)
2696        .and_then(|n| n.utf8_text(source).ok())
2697        .unwrap_or("");
2698    if !obj.is_empty() && !attr.is_empty() {
2699        push_method_call_ref(obj, attr, refs, row);
2700    }
2701}
2702
2703fn push_method_call_ref(obj: &str, method: &str, refs: &mut Vec<AstRef>, row: usize) {
2704    refs.push(AstRef {
2705        kind: AstRefKind::MethodCall {
2706            receiver: obj.to_string(),
2707            method: method.to_string(),
2708        },
2709        row,
2710    });
2711}
2712
2713/// Resolve a single reference against scopes and symbol tables.
2714fn resolve_ref(
2715    ast_ref: &AstRef,
2716    scope_idx: usize,
2717    scopes: &[Scope],
2718    symbol_table: &HashMap<String, Vec<String>>,
2719    class_members: &HashMap<String, Vec<(String, String)>>,
2720    import_table: &HashMap<(String, String), String>,
2721    instance_attr_types: &HashMap<(String, String), String>,
2722    entity_map: &HashMap<String, EntityInfo>,
2723    file_path: &str,
2724    from_entity_id: &str,
2725) -> Option<(String, RefType, &'static str)> {
2726    match &ast_ref.kind {
2727        AstRefKind::Call(name) => {
2728            // 1. Walk scope chain for the name
2729            if let Some(eid) = lookup_scope_chain(scope_idx, scopes, name) {
2730                if eid != from_entity_id {
2731                    return Some((eid, RefType::Calls, "scope_chain"));
2732                }
2733            }
2734
2735            // 2. Check import table
2736            let key = (file_path.to_string(), name.clone());
2737            if let Some(target_id) = import_table.get(&key) {
2738                return Some((target_id.clone(), RefType::Calls, "import"));
2739            }
2740
2741            // 3. Global symbol table fallback (constructor calls or cross-file functions)
2742            if let Some(target_ids) = symbol_table.get(name.as_str()) {
2743                let is_constructor = name.chars().next().map_or(false, |c| c.is_uppercase());
2744                let ref_type = if is_constructor { RefType::TypeRef } else { RefType::Calls };
2745                // Prefer same-file, then any
2746                let target = target_ids
2747                    .iter()
2748                    .find(|id| {
2749                        entity_map
2750                            .get(*id)
2751                            .map_or(false, |e| e.file_path == file_path)
2752                    })
2753                    .or_else(|| {
2754                        // Only fall back to cross-file global for constructor calls.
2755                        // Lowercase calls require explicit imports to avoid false positives.
2756                        if is_constructor {
2757                            target_ids.first()
2758                        } else {
2759                            None
2760                        }
2761                    });
2762                if let Some(tid) = target {
2763                    return Some((tid.clone(), ref_type, "scope_chain"));
2764                }
2765            }
2766
2767            None
2768        }
2769
2770        AstRefKind::MethodCall { receiver, method } => {
2771            if receiver == "self" || receiver == "this" {
2772                // self.method() -> find in enclosing class
2773                let mut idx = scope_idx;
2774                loop {
2775                    if scopes[idx].kind == "class" {
2776                        if let Some(eid) = scopes[idx].defs.get(method.as_str()) {
2777                            return Some((eid.clone(), RefType::Calls, "scope_chain"));
2778                        }
2779                        break;
2780                    }
2781                    match scopes[idx].parent {
2782                        Some(p) => idx = p,
2783                        None => break,
2784                    }
2785                }
2786                return None;
2787            }
2788
2789            // Handle chained self.attr.method() pattern
2790            // receiver is "self.X" where X is an instance attribute
2791            if receiver.starts_with("self.") || receiver.starts_with("this.") {
2792                let attr_name = &receiver[5..]; // strip "self." or "this."
2793                // Find the enclosing class name
2794                let class_name = find_enclosing_class(scope_idx, scopes, entity_map);
2795                if let Some(cn) = class_name {
2796                    // Look up instance attribute type
2797                    if let Some(attr_type) = instance_attr_types.get(&(cn, attr_name.to_string())) {
2798                        if let Some(members) = class_members.get(attr_type.as_str()) {
2799                            if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2800                                return Some((mid.clone(), RefType::Calls, "type_tracking"));
2801                            }
2802                        }
2803                    }
2804                }
2805            }
2806
2807            // Handle chained var.field.method() pattern (e.g. Go receiver: t.Conn.Execute())
2808            if receiver.contains('.') && !receiver.starts_with("self.") && !receiver.starts_with("this.") {
2809                if let Some(dot_pos) = receiver.find('.') {
2810                    let var_part = &receiver[..dot_pos];
2811                    let field_part = &receiver[dot_pos + 1..];
2812                    if let Some(var_type) = lookup_type_in_scopes(scope_idx, scopes, var_part) {
2813                        if let Some(attr_type) = instance_attr_types.get(&(var_type, field_part.to_string())) {
2814                            if let Some(members) = class_members.get(attr_type.as_str()) {
2815                                if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2816                                    return Some((mid.clone(), RefType::Calls, "type_tracking"));
2817                                }
2818                            }
2819                        }
2820                    }
2821                }
2822            }
2823
2824            // receiver.method() -> look up receiver type, then resolve method
2825            let receiver_type = lookup_type_in_scopes(scope_idx, scopes, receiver);
2826
2827            if let Some(class_name) = receiver_type {
2828                if let Some(members) = class_members.get(class_name.as_str()) {
2829                    if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
2830                        return Some((mid.clone(), RefType::Calls, "type_tracking"));
2831                    }
2832                }
2833            }
2834
2835            // Fallback: check import table for the receiver
2836            let key = (file_path.to_string(), receiver.clone());
2837            if let Some(target_id) = import_table.get(&key) {
2838                if let Some(info) = entity_map.get(target_id) {
2839                    if matches!(info.entity_type.as_str(), "class" | "struct") {
2840                        if let Some(members) = class_members.get(&info.name) {
2841                            if let Some((_, mid)) =
2842                                members.iter().find(|(n, _)| n == method)
2843                            {
2844                                return Some((
2845                                    mid.clone(),
2846                                    RefType::Calls,
2847                                    "type_tracking",
2848                                ));
2849                            }
2850                        }
2851                    }
2852                }
2853            }
2854
2855            // Go package-qualified call: package.Function()
2856            // Try the method name directly in the import table
2857            let key = (file_path.to_string(), method.clone());
2858            if let Some(target_id) = import_table.get(&key) {
2859                return Some((target_id.clone(), RefType::Calls, "import"));
2860            }
2861
2862            None
2863        }
2864    }
2865}
2866
2867/// Find the class name for the enclosing class scope.
2868fn find_enclosing_class(
2869    start_scope: usize,
2870    scopes: &[Scope],
2871    entity_map: &HashMap<String, EntityInfo>,
2872) -> Option<String> {
2873    let mut idx = start_scope;
2874    loop {
2875        if scopes[idx].kind == "class" {
2876            if let Some(ref oid) = scopes[idx].owner_id {
2877                return entity_map.get(oid).map(|e| e.name.clone());
2878            }
2879        }
2880        match scopes[idx].parent {
2881            Some(p) => idx = p,
2882            None => return None,
2883        }
2884    }
2885}
2886
2887/// Walk up the scope chain looking for a definition.
2888fn lookup_scope_chain(
2889    start_scope: usize,
2890    scopes: &[Scope],
2891    name: &str,
2892) -> Option<String> {
2893    let mut idx = start_scope;
2894    loop {
2895        if let Some(eid) = scopes[idx].defs.get(name) {
2896            return Some(eid.clone());
2897        }
2898        match scopes[idx].parent {
2899            Some(p) => idx = p,
2900            None => return None,
2901        }
2902    }
2903}
2904
2905/// Walk up the scope chain looking for a type binding.
2906fn lookup_type_in_scopes(
2907    start_scope: usize,
2908    scopes: &[Scope],
2909    var_name: &str,
2910) -> Option<String> {
2911    let mut idx = start_scope;
2912    loop {
2913        if let Some(type_name) = scopes[idx].types.get(var_name) {
2914            return Some(type_name.clone());
2915        }
2916        match scopes[idx].parent {
2917            Some(p) => idx = p,
2918            None => return None,
2919        }
2920    }
2921}
2922
2923fn is_builtin(name: &str, config: &ScopeResolveConfig) -> bool {
2924    // Common builtins across languages
2925    if matches!(name, "None" | "True" | "False" | "null" | "undefined" | "nil") {
2926        return true;
2927    }
2928    config.builtins.contains(&name)
2929}