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