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