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