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