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, HashSet};
15use std::path::Path;
16use std::sync::Arc;
17
18#[cfg(feature = "parallel")]
19use rayon::prelude::*;
20
21use crate::model::entity::SemanticEntity;
22
23macro_rules! maybe_par_iter {
24    ($slice:expr) => {{
25        #[cfg(feature = "parallel")]
26        { $slice.par_iter() }
27        #[cfg(not(feature = "parallel"))]
28        { $slice.iter() }
29    }};
30}
31use crate::parser::graph::{EntityInfo, RefType};
32use crate::parser::import_resolution::{find_import_target, import_source_matches_file};
33use crate::parser::plugins::code::languages::{
34    get_language_config, AssignmentStrategy, CallNodeStyle, ClassNameField, InitStrategy,
35    ParamNameField, ScopeResolveConfig,
36};
37
38/// A scope in the scope tree. Scopes are nested: module -> class -> function -> block.
39pub struct Scope {
40    parent: Option<usize>,
41    /// Definitions visible in this scope: name -> entity_id
42    defs: HashMap<String, String>,
43    /// Local bindings that shadow outer names but are not graph entities.
44    bindings: HashSet<String>,
45    /// Variable type bindings: var_name -> class_name (from `x = Foo()`)
46    types: HashMap<String, String>,
47    /// Unresolved call assignments: var_name -> function_name (from `x = func()`)
48    /// These get resolved after return type analysis.
49    pending_call_types: HashMap<String, String>,
50    /// Which entity owns this scope (if any)
51    owner_id: Option<String>,
52    /// What kind of scope: "module", "class", "function"
53    kind: &'static str,
54}
55
56/// Reference found in the AST
57struct AstRef {
58    /// Kind of reference
59    kind: AstRefKind,
60    /// Row (0-indexed) where this reference appears in the source
61    row: usize,
62}
63
64enum AstRefKind {
65    /// Bare name call: `foo()`
66    Call(String),
67    /// Attribute call: `x.method()`
68    MethodCall { receiver: String, method: String },
69}
70
71/// Result of scope-aware resolution
72pub struct ScopeResult {
73    pub edges: Vec<(String, String, RefType)>,
74    /// Debug info: which references were resolved and how
75    pub resolution_log: Vec<ResolutionEntry>,
76}
77
78#[derive(Clone)]
79pub struct ResolutionEntry {
80    pub from_entity: String,
81    pub reference: String,
82    pub resolved_to: Option<String>,
83    pub method: &'static str, // "scope_chain", "type_tracking", "import", "unresolved"
84}
85
86/// Resolve references using tree-sitter scope analysis.
87///
88/// For each file:
89/// 1. Parse with tree-sitter
90/// 2. Build a scope tree (module -> class -> function)
91/// 3. Walk entity AST subtrees to find reference nodes
92/// 4. Resolve each reference via scope chain + type tracking
93/// Pre-built lookup tables that can be shared between `EntityGraph::build()` and
94/// `resolve_with_scopes()` to avoid redundant O(E) passes.
95pub(crate) struct PreBuiltLookups {
96    pub(crate) symbol_table: Arc<HashMap<String, Vec<String>>>,
97    pub(crate) class_members: HashMap<String, Vec<(String, String)>>,
98    pub(crate) entity_ranges: HashMap<String, Vec<(usize, usize, String)>>,
99    /// Go package index: pkg_name → [(entity_name, entity_id)]
100    /// Avoids O(symbol_table) scan per Go import.
101    pub(crate) go_pkg_index: HashMap<String, Vec<(String, String)>>,
102}
103
104/// Public API — preserves the original 5-parameter signature for semver compatibility.
105pub fn resolve_with_scopes(
106    root: &Path,
107    file_paths: &[String],
108    all_entities: &[SemanticEntity],
109    entity_map: &HashMap<String, EntityInfo>,
110    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
111) -> ScopeResult {
112    resolve_with_scopes_full(root, file_paths, all_entities, entity_map, pre_parsed, None)
113}
114
115/// Internal version with pre-built lookups for performance.
116pub(crate) fn resolve_with_scopes_full(
117    root: &Path,
118    file_paths: &[String],
119    all_entities: &[SemanticEntity],
120    entity_map: &HashMap<String, EntityInfo>,
121    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
122    pre_built: Option<PreBuiltLookups>,
123) -> ScopeResult {
124    let mut all_edges: Vec<(String, String, RefType)> = Vec::new();
125    let mut log: Vec<ResolutionEntry> = Vec::new();
126
127    // Use pre-built lookups if provided, otherwise build from scratch
128    let (symbol_table, class_members, entity_ranges, go_pkg_index) = if let Some(pb) = pre_built {
129        (pb.symbol_table, pb.class_members, pb.entity_ranges, pb.go_pkg_index)
130    } else {
131        let mut symbol_table: HashMap<String, Vec<String>> = HashMap::new();
132        let mut class_members: HashMap<String, Vec<(String, String)>> = HashMap::new();
133        let mut entity_ranges: HashMap<String, Vec<(usize, usize, String)>> = HashMap::new();
134
135        for entity in all_entities {
136            symbol_table
137                .entry(entity.name.clone())
138                .or_default()
139                .push(entity.id.clone());
140
141            if let Some(ref pid) = entity.parent_id {
142                if let Some(parent) = entity_map.get(pid) {
143                    if matches!(
144                        parent.entity_type.as_str(),
145                        "class" | "struct" | "interface" | "impl"
146                            | "enum" | "protocol_declaration"
147                            | "object_declaration" | "companion_object"
148                    ) {
149                        class_members
150                            .entry(parent.name.clone())
151                            .or_default()
152                            .push((entity.name.clone(), entity.id.clone()));
153                    }
154                }
155            }
156
157            if entity.entity_type == "method" && entity.file_path.ends_with(".go") {
158                if let Some(struct_name) = extract_go_receiver_type(&entity.content) {
159                    class_members
160                        .entry(struct_name)
161                        .or_default()
162                        .push((entity.name.clone(), entity.id.clone()));
163                }
164            }
165
166            entity_ranges
167                .entry(entity.file_path.clone())
168                .or_default()
169                .push((entity.start_line, entity.end_line, entity.id.clone()));
170        }
171
172        // Build Go package index for O(1) import lookup
173        let go_pkg_index = build_go_pkg_index(&symbol_table, entity_map);
174
175        (Arc::new(symbol_table), class_members, entity_ranges, go_pkg_index)
176    };
177
178    // Build file-path indexed entity lookup: file_path -> Vec<&SemanticEntity>
179    let mut entities_by_file: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
180    for entity in all_entities {
181        entities_by_file
182            .entry(entity.file_path.as_str())
183            .or_default()
184            .push(entity);
185    }
186
187    // Build parent_id indexed entity lookup: parent_id -> Vec<&SemanticEntity>
188    let mut children_by_parent: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
189    for entity in all_entities {
190        if let Some(ref pid) = entity.parent_id {
191            children_by_parent
192                .entry(pid.as_str())
193                .or_default()
194                .push(entity);
195        }
196    }
197
198    // Return type map: function_entity_id -> class_name (if function returns ClassName())
199    let mut return_type_map: HashMap<String, String> = HashMap::new();
200
201    // Instance attribute types: (class_name, attr_name) -> class_name_of_attr
202    let mut instance_attr_types: HashMap<(String, String), String> = HashMap::new();
203
204    // __init__ param info: class_name -> (ordered_params, attr_to_param mapping)
205    // attr_to_param: attr_name -> param_name (for self.attr = param patterns)
206    let mut init_params: HashMap<String, Vec<String>> = HashMap::new();
207    let mut attr_to_param: HashMap<(String, String), String> = HashMap::new();
208
209    // Merge pre-parsed trees with disk-parsed trees for missing files
210    let mut owned_parsed_files: Vec<(String, String, tree_sitter::Tree)> = Vec::new();
211    let pre_set: std::collections::HashSet<String> = if let Some(pp) = pre_parsed {
212        let set = pp.iter().map(|(fp, _, _)| fp.clone()).collect();
213        owned_parsed_files = pp;
214        set
215    } else {
216        std::collections::HashSet::new()
217    };
218    // Parse any files not already in the pre-parsed set
219    for file_path in file_paths {
220        if pre_set.contains(file_path) {
221            continue;
222        }
223        let full_path = root.join(file_path);
224        let content = match std::fs::read_to_string(&full_path) {
225            Ok(c) => c,
226            Err(_) => continue,
227        };
228        let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
229        let config = match get_language_config(ext) {
230            Some(c) => c,
231            None => continue,
232        };
233        let language = match (config.get_language)() {
234            Some(l) => l,
235            None => continue,
236        };
237        let mut parser = tree_sitter::Parser::new();
238        let _ = parser.set_language(&language);
239        if let Some(tree) = parser.parse(content.as_bytes(), None) {
240            owned_parsed_files.push((file_path.clone(), content, tree));
241        }
242    }
243    let parsed_files: &[(String, String, tree_sitter::Tree)] = &owned_parsed_files;
244
245    // Pass 1: Scan ALL files for return types and instance attr types first
246    // This ensures cross-file return type info is available during resolution
247    // Parallelized: each file produces local maps, then merged sequentially.
248    let pass1_results: Vec<(
249        HashMap<String, String>,
250        HashMap<(String, String), String>,
251        HashMap<String, Vec<String>>,
252        HashMap<(String, String), String>,
253    )> = maybe_par_iter!(parsed_files)
254        .filter_map(|(file_path, content, tree)| {
255            let source = content.as_bytes();
256            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
257            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
258
259            let file_entities = entities_by_file.get(file_path.as_str()).map(|v| v.as_slice()).unwrap_or(&[]);
260
261            let mut local_return_type_map: HashMap<String, String> = HashMap::new();
262            scan_return_types(
263                tree.root_node(),
264                file_path,
265                file_entities,
266                source,
267                &mut local_return_type_map,
268                config,
269            );
270
271            let mut local_instance_attr_types: HashMap<(String, String), String> = HashMap::new();
272            let mut local_init_params: HashMap<String, Vec<String>> = HashMap::new();
273            let mut local_attr_to_param: HashMap<(String, String), String> = HashMap::new();
274            scan_init_self_attrs(
275                tree.root_node(),
276                file_path,
277                file_entities,
278                entity_map,
279                source,
280                &mut local_instance_attr_types,
281                &mut local_init_params,
282                &mut local_attr_to_param,
283                config,
284            );
285
286            Some((local_return_type_map, local_instance_attr_types, local_init_params, local_attr_to_param))
287        })
288        .collect();
289
290    for (local_rtm, local_iat, local_ip, local_atp) in pass1_results {
291        return_type_map.extend(local_rtm);
292        instance_attr_types.extend(local_iat);
293        init_params.extend(local_ip);
294        attr_to_param.extend(local_atp);
295    }
296
297    // Pass 1b: Infer constructor parameter types from call sites
298    // For `Transaction(get_connection())`, infer conn param has type Connection.
299    // Then resolve self.conn = conn -> (Transaction, conn) -> Connection
300    infer_constructor_param_types(
301        parsed_files,
302        &return_type_map,
303        &init_params,
304        &attr_to_param,
305        &symbol_table,
306        entity_map,
307        &mut instance_attr_types,
308    );
309
310    // Pass 2: Build scopes, imports, and resolve references per file (parallel)
311    let per_file_results: Vec<(Vec<(String, String, RefType)>, Vec<ResolutionEntry>)> = maybe_par_iter!(parsed_files)
312        .filter_map(|(file_path, content, tree)| {
313            let source = content.as_bytes();
314            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
315            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
316
317            let mut scopes: Vec<Scope> = vec![Scope {
318                parent: None,
319                defs: HashMap::new(),
320                bindings: HashSet::new(),
321                types: HashMap::new(),
322                pending_call_types: HashMap::new(),
323                owner_id: None,
324                kind: "module",
325            }];
326
327            let mut entity_scope_map: HashMap<String, usize> = HashMap::new();
328            let mut entity_inner_scope: HashMap<String, usize> = HashMap::new();
329
330            if let Some(ranges) = entity_ranges.get(file_path.as_str()) {
331                for (_start, _end, eid) in ranges {
332                    if let Some(info) = entity_map.get(eid) {
333                        if info.parent_id.is_none() {
334                            scopes[0].defs.insert(info.name.clone(), eid.clone());
335                            entity_scope_map.insert(eid.clone(), 0);
336                        }
337                    }
338                }
339            }
340
341            let file_entities: Vec<&SemanticEntity> = entities_by_file
342                .get(file_path.as_str())
343                .map(|v| v.as_slice())
344                .unwrap_or(&[])
345                .to_vec();
346
347            build_scopes_from_ast(
348                tree.root_node(),
349                0,
350                &mut scopes,
351                &mut entity_scope_map,
352                &mut entity_inner_scope,
353                &file_entities,
354                &children_by_parent,
355                entity_map,
356                file_path,
357                source,
358                config,
359            );
360
361            let mut local_import_table: HashMap<(String, String), String> = HashMap::new();
362            extract_imports_from_ast(
363                tree.root_node(),
364                file_path,
365                source,
366                &symbol_table,
367                entity_map,
368                &mut local_import_table,
369                &mut scopes,
370                config,
371                &go_pkg_index,
372            );
373
374            // Resolve pending call types using the complete return type map
375            inject_return_type_bindings(
376                &entity_inner_scope,
377                &mut scopes,
378                &return_type_map,
379                &local_import_table,
380                file_path,
381                entity_map,
382            );
383
384            let mut file_edges: Vec<(String, String, RefType)> = Vec::new();
385            let mut file_log: Vec<ResolutionEntry> = Vec::new();
386
387            // Walk the AST once for the entire file, collecting all refs with row positions
388            let all_file_refs = collect_all_file_refs(tree.root_node(), source, config);
389
390            for entity in &file_entities {
391                let scope_idx = entity_inner_scope
392                    .get(&entity.id)
393                    .or_else(|| entity_scope_map.get(&entity.id))
394                    .copied()
395                    .unwrap_or(0);
396
397                let start_row = entity.start_line.saturating_sub(1); // 1-indexed to 0-indexed
398                let end_row = entity.end_line; // exclusive
399
400                // Filter pre-collected refs to this entity's line range
401                for ast_ref in all_file_refs.iter().filter(|r| r.row >= start_row && r.row < end_row) {
402                    // Skip self-name refs (was previously done during collection)
403                    let is_self_ref = match &ast_ref.kind {
404                        AstRefKind::Call(name) => name == &entity.name,
405                        AstRefKind::MethodCall { .. } => false,
406                    };
407                    if is_self_ref {
408                        continue;
409                    }
410
411                    // Languages without per-symbol imports (e.g. Swift, Kotlin)
412                    // allow cross-file resolution for lowercase function names.
413                    let allow_cross_file = config.import_extractor.is_none();
414                    let resolution = resolve_ref(
415                        ast_ref,
416                        scope_idx,
417                        &scopes,
418                        &symbol_table,
419                        &class_members,
420                        &local_import_table,
421                        &instance_attr_types,
422                        entity_map,
423                        file_path,
424                        &entity.id,
425                        allow_cross_file,
426                    );
427
428                    if let Some((target_id, ref_type, method)) = resolution {
429                        if target_id != entity.id {
430                            let is_parent_child = entity
431                                .parent_id
432                                .as_ref()
433                                .map_or(false, |pid| pid == &target_id || entity_map.get(&target_id).map_or(false, |t| t.parent_id.as_ref() == Some(&entity.id)));
434
435                            if !is_parent_child {
436                                file_edges.push((
437                                    entity.id.clone(),
438                                    target_id.clone(),
439                                    ref_type,
440                                ));
441                                file_log.push(ResolutionEntry {
442                                    from_entity: entity.id.clone(),
443                                    reference: ref_description(ast_ref),
444                                    resolved_to: Some(target_id),
445                                    method,
446                                });
447                            }
448                        }
449                    } else {
450                        file_log.push(ResolutionEntry {
451                            from_entity: entity.id.clone(),
452                            reference: ref_description(ast_ref),
453                            resolved_to: None,
454                            method: "unresolved",
455                        });
456                    }
457                }
458            }
459
460            Some((file_edges, file_log))
461        })
462        .collect();
463
464    for (file_edges, file_log) in per_file_results {
465        all_edges.extend(file_edges);
466        log.extend(file_log);
467    }
468
469    // Deduplicate edges
470    let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::with_capacity(all_edges.len());
471    let deduped_edges: Vec<(String, String, RefType)> = {
472        let mut result = Vec::with_capacity(all_edges.len());
473        for edge in all_edges {
474            if seen.insert((edge.0.clone(), edge.1.clone())) {
475                result.push(edge);
476            }
477        }
478        result
479    };
480    let all_edges = deduped_edges;
481
482    ScopeResult {
483        edges: all_edges,
484        resolution_log: log,
485    }
486}
487
488fn ref_description(ast_ref: &AstRef) -> String {
489    match &ast_ref.kind {
490        AstRefKind::Call(name) => format!("{}()", name),
491        AstRefKind::MethodCall { receiver, method } => format!("{}.{}()", receiver, method),
492    }
493}
494
495/// Build scope tree by walking the AST.
496/// Creates class scopes and maps methods to them.
497/// Uses an iterative worklist to avoid stack overflow on deeply nested ASTs.
498/// Fixes: https://github.com/Ataraxy-Labs/sem/issues/103
499fn build_scopes_from_ast(
500    root: tree_sitter::Node,
501    root_scope: usize,
502    scopes: &mut Vec<Scope>,
503    entity_scope_map: &mut HashMap<String, usize>,
504    entity_inner_scope: &mut HashMap<String, usize>,
505    file_entities: &[&SemanticEntity],
506    children_by_parent: &HashMap<&str, Vec<&SemanticEntity>>,
507    entity_map: &HashMap<String, EntityInfo>,
508    _file_path: &str,
509    source: &[u8],
510    config: &ScopeResolveConfig,
511) {
512    // Each entry: (node, current_scope)
513    let mut worklist: Vec<(tree_sitter::Node, usize)> = vec![(root, root_scope)];
514
515    while let Some((node, current_scope)) = worklist.pop() {
516        let kind = node.kind();
517
518        // Class-like scope: config-driven
519        let is_class_like = config.class_scope_nodes.contains(&kind);
520
521        // Impl scope: config-driven (Rust impl_item, Swift extension)
522        let is_impl = config.impl_scope_nodes.contains(&kind);
523
524        if is_class_like || is_impl {
525            let class_name = if is_impl {
526                node.child_by_field_name("type")
527                    .and_then(|n| n.utf8_text(source).ok())
528                    .unwrap_or("")
529            } else {
530                match &config.class_name_field {
531                    ClassNameField::Simple(field) => {
532                        node.child_by_field_name(field)
533                            .and_then(|n| n.utf8_text(source).ok())
534                            .unwrap_or("")
535                    }
536                    ClassNameField::TypeSpec { spec_kind, field } => {
537                        let mut name = "";
538                        let mut cursor = node.walk();
539                        for child in node.named_children(&mut cursor) {
540                            if child.kind() == *spec_kind {
541                                name = child
542                                    .child_by_field_name(field)
543                                    .and_then(|n| n.utf8_text(source).ok())
544                                    .unwrap_or("");
545                                break;
546                            }
547                        }
548                        name
549                    }
550                    ClassNameField::ImplType(field) => {
551                        node.child_by_field_name(field)
552                            .and_then(|n| n.utf8_text(source).ok())
553                            .unwrap_or("")
554                    }
555                }
556            };
557
558            let class_entity = file_entities.iter().find(|e| {
559                e.name == class_name
560                    && matches!(
561                        e.entity_type.as_str(),
562                        "class" | "struct" | "interface"
563                            | "enum" | "protocol_declaration"
564                            | "object_declaration" | "companion_object"
565                    )
566            }).copied();
567
568            if let Some(ce) = class_entity {
569                let existing_scope = entity_inner_scope.get(&ce.id).copied();
570
571                let class_scope_idx = if let Some(idx) = existing_scope {
572                    idx
573                } else {
574                    let idx = scopes.len();
575                    scopes.push(Scope {
576                        parent: Some(current_scope),
577                        defs: HashMap::new(),
578                        bindings: HashSet::new(),
579                        types: HashMap::new(),
580                        pending_call_types: HashMap::new(),
581                        owner_id: Some(ce.id.clone()),
582                        kind: "class",
583                    });
584                    entity_scope_map.insert(ce.id.clone(), current_scope);
585                    entity_inner_scope.insert(ce.id.clone(), idx);
586                    idx
587                };
588
589                if let Some(children) = children_by_parent.get(ce.id.as_str()) {
590                    for entity in children {
591                        scopes[class_scope_idx]
592                            .defs
593                            .insert(entity.name.clone(), entity.id.clone());
594                        entity_scope_map.insert(entity.id.clone(), class_scope_idx);
595                    }
596                }
597
598                let mut cursor = node.walk();
599                let children: Vec<_> = node.named_children(&mut cursor).collect();
600                for child in children.into_iter().rev() {
601                    worklist.push((child, class_scope_idx));
602                }
603                continue;
604            } else if !is_impl {
605                let class_scope_idx = scopes.len();
606                scopes.push(Scope {
607                    parent: Some(current_scope),
608                    defs: HashMap::new(),
609                    bindings: HashSet::new(),
610                    types: HashMap::new(),
611                    pending_call_types: HashMap::new(),
612                    owner_id: None,
613                    kind: "class",
614                });
615                let mut cursor = node.walk();
616                let children: Vec<_> = node.named_children(&mut cursor).collect();
617                for child in children.into_iter().rev() {
618                    worklist.push((child, class_scope_idx));
619                }
620                continue;
621            }
622        }
623
624        // Rust mod_item: create a module scope so nested functions resolve
625        // names from the parent scope (e.g. super::target() walks up correctly).
626        if kind == "mod_item" {
627            let mod_name = node
628                .child_by_field_name("name")
629                .and_then(|n| n.utf8_text(source).ok())
630                .unwrap_or("");
631            let mod_scope_idx = scopes.len();
632            scopes.push(Scope {
633                parent: Some(current_scope),
634                defs: HashMap::new(),
635                bindings: HashSet::new(),
636                types: HashMap::new(),
637                pending_call_types: HashMap::new(),
638                owner_id: None,
639                kind: "module",
640            });
641
642            // Register any entities that are children of this module
643            let mod_entity = file_entities.iter().find(|e| {
644                e.name == mod_name
645                    && e.entity_type == "module"
646                    && {
647                        let line = node.start_position().row + 1;
648                        e.start_line <= line && line <= e.end_line
649                    }
650            }).copied();
651
652            if let Some(me) = mod_entity {
653                scopes[mod_scope_idx].owner_id = Some(me.id.clone());
654                entity_scope_map.entry(me.id.clone()).or_insert(current_scope);
655                entity_inner_scope.insert(me.id.clone(), mod_scope_idx);
656
657                // Register child entities in the module scope
658                if let Some(children) = children_by_parent.get(me.id.as_str()) {
659                    for child_entity in children {
660                        scopes[mod_scope_idx]
661                            .defs
662                            .insert(child_entity.name.clone(), child_entity.id.clone());
663                        entity_scope_map.insert(child_entity.id.clone(), mod_scope_idx);
664                    }
665                }
666            }
667
668            let mut cursor = node.walk();
669            let children: Vec<_> = node.named_children(&mut cursor).collect();
670            for child in children.into_iter().rev() {
671                worklist.push((child, mod_scope_idx));
672            }
673            continue;
674        }
675
676        // Function-like scope: config-driven
677        let is_function_like = config.function_scope_nodes.contains(&kind);
678
679        if is_function_like {
680            let func_name = node.child_by_field_name("name")
681                .and_then(|n| n.utf8_text(source).ok())
682                .unwrap_or("");
683
684            let parent_scope = if config.external_method && kind == "method_declaration" {
685                let receiver_type = node.utf8_text(source).ok()
686                    .and_then(|t| extract_go_receiver_type(t));
687                if let Some(ref struct_name) = receiver_type {
688                    let found = scopes.iter().enumerate().find(|(_, s)| {
689                        s.kind == "class" && s.owner_id.as_ref().map_or(false, |oid| {
690                            entity_map.get(oid).map_or(false, |e| e.name == *struct_name)
691                        })
692                    });
693                    found.map(|(idx, _)| idx).unwrap_or(current_scope)
694                } else {
695                    current_scope
696                }
697            } else {
698                current_scope
699            };
700
701            let func_scope_idx = scopes.len();
702            scopes.push(Scope {
703                parent: Some(parent_scope),
704                defs: HashMap::new(),
705                bindings: HashSet::new(),
706                types: HashMap::new(),
707                pending_call_types: HashMap::new(),
708                owner_id: None,
709                kind: "function",
710            });
711
712            let func_entity = file_entities.iter().find(|e| {
713                e.name == func_name && {
714                    let line = node.start_position().row + 1;
715                    e.start_line <= line && line <= e.end_line
716                }
717            }).copied();
718
719            if let Some(fe) = func_entity {
720                scopes[func_scope_idx].owner_id = Some(fe.id.clone());
721                entity_scope_map.entry(fe.id.clone()).or_insert(parent_scope);
722                entity_inner_scope.insert(fe.id.clone(), func_scope_idx);
723                if config.external_method && kind == "method_declaration" && parent_scope != current_scope {
724                    scopes[parent_scope].defs.insert(fe.name.clone(), fe.id.clone());
725                }
726            }
727
728            scan_assignments(node, func_scope_idx, scopes, source, config);
729            scan_function_params(node, func_scope_idx, scopes, source, config);
730
731            if config.external_method && kind == "method_declaration" {
732                if let Some(receiver) = node.child_by_field_name("receiver") {
733                    let mut rcursor = receiver.walk();
734                    for param in receiver.named_children(&mut rcursor) {
735                        if param.kind() == "parameter_declaration" {
736                            let param_name = param
737                                .child_by_field_name("name")
738                                .and_then(|n| n.utf8_text(source).ok())
739                                .unwrap_or("");
740                            let param_type = param
741                                .child_by_field_name("type")
742                                .map(|n| extract_base_type(n, source))
743                                .unwrap_or_default();
744                            if !param_name.is_empty() && !param_type.is_empty() {
745                                scopes[func_scope_idx]
746                                    .types
747                                    .insert(param_name.to_string(), param_type);
748                            }
749                        }
750                    }
751                }
752            }
753
754            let mut cursor = node.walk();
755            let children: Vec<_> = node.named_children(&mut cursor).collect();
756            for child in children.into_iter().rev() {
757                worklist.push((child, func_scope_idx));
758            }
759            continue;
760        }
761
762        let mut cursor = node.walk();
763        let children: Vec<_> = node.named_children(&mut cursor).collect();
764        for child in children.into_iter().rev() {
765            worklist.push((child, current_scope));
766        }
767    }
768}
769
770/// Scan for variable assignments and record type bindings.
771fn scan_assignments(
772    root: tree_sitter::Node,
773    scope_idx: usize,
774    scopes: &mut Vec<Scope>,
775    source: &[u8],
776    config: &ScopeResolveConfig,
777) {
778    let mut worklist = vec![root];
779    while let Some(node) = worklist.pop() {
780        let mut cursor = node.walk();
781        for child in node.named_children(&mut cursor) {
782            let ck = child.kind();
783
784            // Check if this node matches an assignment rule
785            for rule in config.assignment_rules {
786                if ck == rule.node_kind {
787                    match rule.strategy {
788                        AssignmentStrategy::LeftRight => {
789                            scan_single_assignment(child, scope_idx, scopes, source);
790                        }
791                        AssignmentStrategy::Declarators => {
792                            scan_ts_var_declaration(child, scope_idx, scopes, source);
793                        }
794                        AssignmentStrategy::PatternBased => {
795                            scan_rust_let_declaration(child, scope_idx, scopes, source);
796                        }
797                        AssignmentStrategy::ShortVar => {
798                            scan_go_short_var(child, scope_idx, scopes, source);
799                        }
800                        AssignmentStrategy::VarSpec => {
801                            scan_go_var_declaration(child, scope_idx, scopes, source);
802                        }
803                    }
804                }
805            }
806
807            // Recurse into configured container nodes
808            if config.assignment_recurse_into.contains(&ck) {
809                worklist.push(child);
810            }
811        }
812    }
813}
814
815/// Scan function parameter type annotations and add them as type bindings.
816/// e.g. `def foo(shelter: Shelter)` -> types["shelter"] = "Shelter"
817fn scan_function_params(
818    node: tree_sitter::Node,
819    scope_idx: usize,
820    scopes: &mut Vec<Scope>,
821    source: &[u8],
822    config: &ScopeResolveConfig,
823) {
824    // Try "parameters" field first (Python, TS, Rust, Go, etc.)
825    // Fallback to direct children for languages like Swift where
826    // params are direct children of function_declaration.
827    let mut params_node = node.child_by_field_name("parameters");
828    if params_node.is_none() {
829        // Kotlin: function_value_parameters
830        let mut c = node.walk();
831        for ch in node.named_children(&mut c) {
832            if ch.kind() == "function_value_parameters" {
833                params_node = Some(ch);
834                break;
835            }
836        }
837    }
838
839    // If we have a params container, iterate its children.
840    // Otherwise, iterate direct children of the function node (Swift).
841    let (iter_node, use_direct) = match params_node {
842        Some(p) => (p, false),
843        None => (node, true),
844    };
845
846    let mut cursor = iter_node.walk();
847    for child in iter_node.named_children(&mut cursor) {
848        // When using direct children, only process param-like nodes
849        if use_direct {
850            let is_param = config.param_rules.iter().any(|r| child.kind() == r.node_kind);
851            if !is_param {
852                continue;
853            }
854        }
855        for rule in config.param_rules {
856            if child.kind() != rule.node_kind {
857                continue;
858            }
859
860            let param_name = match &rule.name_field {
861                ParamNameField::Simple(field) => {
862                    child.child_by_field_name(field)
863                        .and_then(|n| n.utf8_text(source).ok())
864                        .unwrap_or("")
865                }
866                ParamNameField::WithFallback(field) => {
867                    child.child_by_field_name(field)
868                        .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
869                        .and_then(|n| n.utf8_text(source).ok())
870                        .unwrap_or("")
871                }
872                ParamNameField::RustPattern => {
873                    child.child_by_field_name("pattern")
874                        .and_then(|n| {
875                            if n.kind() == "identifier" {
876                                n.utf8_text(source).ok()
877                            } else if n.kind() == "mut_pattern" {
878                                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
879                            } else if n.kind() == "reference_pattern" {
880                                n.named_child(0).and_then(|c| {
881                                    if c.kind() == "identifier" {
882                                        c.utf8_text(source).ok()
883                                    } else if c.kind() == "mut_pattern" {
884                                        c.named_child(0).and_then(|cc| cc.utf8_text(source).ok())
885                                    } else {
886                                        None
887                                    }
888                                })
889                            } else {
890                                None
891                            }
892                        })
893                        .unwrap_or("")
894                }
895            };
896
897            if param_name.is_empty() || rule.skip_names.contains(&param_name) {
898                continue;
899            }
900            scopes[scope_idx].bindings.insert(param_name.to_string());
901
902            // Try the configured type field first, then fall back to child type nodes
903            // (Swift parameters have user_type children instead of a "type" field)
904            let mut type_node = child.child_by_field_name(rule.type_field);
905            if type_node.is_none() {
906                let mut tc = child.walk();
907                for ch in child.named_children(&mut tc) {
908                    if matches!(ch.kind(), "user_type" | "type_annotation" | "type_identifier") {
909                        type_node = Some(ch);
910                        break;
911                    }
912                }
913            }
914            if let Some(tn) = type_node {
915                let type_text = extract_base_type(tn, source);
916                if !type_text.is_empty()
917                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
918                {
919                    scopes[scope_idx]
920                        .types
921                        .insert(param_name.to_string(), type_text);
922                }
923            }
924        }
925    }
926}
927
928/// Python/TS: `x = Foo()` or `x = func()`
929fn scan_single_assignment(
930    node: tree_sitter::Node,
931    scope_idx: usize,
932    scopes: &mut Vec<Scope>,
933    source: &[u8],
934) {
935    let assign = if node.kind() == "assignment" {
936        node
937    } else {
938        let mut cursor = node.walk();
939        let children: Vec<_> = node.named_children(&mut cursor).collect();
940        match children.into_iter().find(|c| c.kind() == "assignment" || c.kind() == "assignment_expression") {
941            Some(a) => a,
942            None => return,
943        }
944    };
945
946    let left = match assign.child_by_field_name("left") {
947        Some(l) => l,
948        None => return,
949    };
950    let right = match assign.child_by_field_name("right") {
951        Some(r) => r,
952        None => return,
953    };
954
955    if left.kind() != "identifier" {
956        return;
957    }
958    let var_name = match left.utf8_text(source) {
959        Ok(n) => n.to_string(),
960        Err(_) => return,
961    };
962    scopes[scope_idx].bindings.insert(var_name.clone());
963
964    record_type_from_rhs(right, &var_name, scope_idx, scopes, source);
965}
966
967/// TS: `const x = new Foo()` or `const x: Type = ...` or `const x = func()`
968/// Also handles Swift `let x = Foo(...)` and Kotlin `val x = Foo(...)`
969fn scan_ts_var_declaration(
970    node: tree_sitter::Node,
971    scope_idx: usize,
972    scopes: &mut Vec<Scope>,
973    source: &[u8],
974) {
975    let mut cursor = node.walk();
976    for child in node.named_children(&mut cursor) {
977        if child.kind() == "variable_declarator" {
978            let var_name = child
979                .child_by_field_name("name")
980                .and_then(|n| n.utf8_text(source).ok())
981                .unwrap_or("")
982                .to_string();
983            if var_name.is_empty() {
984                continue;
985            }
986            scopes[scope_idx].bindings.insert(var_name.clone());
987
988            // Check for explicit type annotation: `const x: Foo = ...`
989            if let Some(type_ann) = child.child_by_field_name("type") {
990                let type_text = extract_base_type(type_ann, source);
991                if !type_text.is_empty()
992                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
993                {
994                    scopes[scope_idx]
995                        .types
996                        .insert(var_name.clone(), type_text);
997                    continue;
998                }
999            }
1000
1001            // Check RHS value
1002            if let Some(value) = child.child_by_field_name("value") {
1003                record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
1004            }
1005        }
1006    }
1007
1008    // Swift: property_declaration has "name" and "value" fields directly,
1009    // or "pattern" child with simple_identifier for the name
1010    // e.g. `let dog = Dog(name: name)`
1011    if node.kind() == "property_declaration" {
1012        // Try "name" field first (Swift uses this), then pattern > simple_identifier
1013        let var_name_opt = node
1014            .child_by_field_name("name")
1015            .and_then(|n| n.utf8_text(source).ok());
1016        let var_name = if let Some(name) = var_name_opt {
1017            name.to_string()
1018        } else {
1019            // Look for pattern > simple_identifier (Swift)
1020            let mut c = node.walk();
1021            let mut found = String::new();
1022            for ch in node.named_children(&mut c) {
1023                if ch.kind() == "pattern" {
1024                    if let Some(id) = ch.named_child(0) {
1025                        if id.kind() == "simple_identifier" || id.kind() == "identifier" {
1026                            if let Ok(name) = id.utf8_text(source) {
1027                                found = name.to_string();
1028                            }
1029                        }
1030                    }
1031                    break;
1032                }
1033            }
1034            found
1035        };
1036
1037        if !var_name.is_empty() {
1038            // Check for type annotation
1039            if let Some(type_ann) = node.child_by_field_name("type") {
1040                let type_text = extract_base_type(type_ann, source);
1041                if !type_text.is_empty()
1042                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1043                {
1044                    scopes[scope_idx].types.insert(var_name.clone(), type_text);
1045                    return;
1046                }
1047            }
1048            // Check RHS value
1049            if let Some(value) = node.child_by_field_name("value") {
1050                record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
1051            } else {
1052                // Swift/Kotlin: value might be a sibling call_expression, not a field
1053                let mut c = node.walk();
1054                for ch in node.named_children(&mut c) {
1055                    if ch.kind() == "call_expression" || ch.kind() == "new_expression" {
1056                        record_type_from_rhs(ch, &var_name, scope_idx, scopes, source);
1057                        break;
1058                    }
1059                }
1060            }
1061            return;
1062        }
1063
1064        // Kotlin: property_declaration > variable_declaration > identifier, then sibling call_expression
1065        let mut c = node.walk();
1066        for child in node.named_children(&mut c) {
1067            if child.kind() == "variable_declaration" {
1068                let var_name_kt = child
1069                    .child_by_field_name("name")
1070                    .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
1071                    .and_then(|n| n.utf8_text(source).ok())
1072                    .unwrap_or("")
1073                    .to_string();
1074
1075                if !var_name_kt.is_empty() {
1076                    // Check for type annotation on the property_declaration
1077                    if let Some(type_ann) = node.child_by_field_name("type") {
1078                        let type_text = extract_base_type(type_ann, source);
1079                        if !type_text.is_empty()
1080                            && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1081                        {
1082                            scopes[scope_idx].types.insert(var_name_kt.clone(), type_text);
1083                            return;
1084                        }
1085                    }
1086                    // Find the value (sibling call_expression or other expression)
1087                    let mut c2 = node.walk();
1088                    for sibling in node.named_children(&mut c2) {
1089                        if sibling.kind() == "call_expression" || sibling.kind() == "new_expression" {
1090                            record_type_from_rhs(sibling, &var_name_kt, scope_idx, scopes, source);
1091                            break;
1092                        }
1093                    }
1094                }
1095                break;
1096            }
1097        }
1098    }
1099}
1100
1101/// Rust: `let x: Type = ...` or `let x = Foo::new()`
1102fn scan_rust_let_declaration(
1103    node: tree_sitter::Node,
1104    scope_idx: usize,
1105    scopes: &mut Vec<Scope>,
1106    source: &[u8],
1107) {
1108    let var_name = node
1109        .child_by_field_name("pattern")
1110        .and_then(|n| {
1111            // Pattern can be just an identifier or `mut x`
1112            if n.kind() == "identifier" {
1113                n.utf8_text(source).ok()
1114            } else if n.kind() == "mut_pattern" {
1115                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
1116            } else {
1117                None
1118            }
1119        })
1120        .unwrap_or("")
1121        .to_string();
1122
1123    if var_name.is_empty() {
1124        return;
1125    }
1126    scopes[scope_idx].bindings.insert(var_name.clone());
1127
1128    // Check for explicit type annotation: `let x: Connection = ...`
1129    if let Some(type_node) = node.child_by_field_name("type") {
1130        let type_text = extract_base_type(type_node, source);
1131        if !type_text.is_empty()
1132            && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1133        {
1134            scopes[scope_idx]
1135                .types
1136                .insert(var_name, type_text);
1137            return;
1138        }
1139    }
1140
1141    // Check RHS value
1142    if let Some(value) = node.child_by_field_name("value") {
1143        record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
1144    }
1145}
1146
1147/// Go: `x := Foo{}` or `x := NewFoo()`
1148fn scan_go_short_var(
1149    node: tree_sitter::Node,
1150    scope_idx: usize,
1151    scopes: &mut Vec<Scope>,
1152    source: &[u8],
1153) {
1154    let left = match node.child_by_field_name("left") {
1155        Some(l) => l,
1156        None => return,
1157    };
1158    let right = match node.child_by_field_name("right") {
1159        Some(r) => r,
1160        None => return,
1161    };
1162
1163    // left is expression_list, right is expression_list
1164    let var_name = if left.kind() == "expression_list" {
1165        left.named_child(0)
1166            .and_then(|n| n.utf8_text(source).ok())
1167            .unwrap_or("")
1168            .to_string()
1169    } else {
1170        left.utf8_text(source).unwrap_or("").to_string()
1171    };
1172
1173    if var_name.is_empty() {
1174        return;
1175    }
1176    scopes[scope_idx].bindings.insert(var_name.clone());
1177
1178    let rhs = if right.kind() == "expression_list" {
1179        match right.named_child(0) {
1180            Some(n) => n,
1181            None => return,
1182        }
1183    } else {
1184        right
1185    };
1186
1187    record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
1188}
1189
1190/// Go: `var x Type = ...` or `var x = Foo{}`
1191fn scan_go_var_declaration(
1192    node: tree_sitter::Node,
1193    scope_idx: usize,
1194    scopes: &mut Vec<Scope>,
1195    source: &[u8],
1196) {
1197    let mut cursor = node.walk();
1198    for child in node.named_children(&mut cursor) {
1199        if child.kind() == "var_spec" {
1200            let var_name = child
1201                .child_by_field_name("name")
1202                .and_then(|n| n.utf8_text(source).ok())
1203                .unwrap_or("")
1204                .to_string();
1205            if var_name.is_empty() {
1206                // Try first named child as name
1207                if let Some(first) = child.named_child(0) {
1208                    if first.kind() == "identifier" {
1209                        let name = first.utf8_text(source).unwrap_or("").to_string();
1210                        if !name.is_empty() {
1211                            scopes[scope_idx].bindings.insert(name.clone());
1212                            // Check for type child
1213                            if let Some(type_node) = child.child_by_field_name("type") {
1214                                let type_text = extract_base_type(type_node, source);
1215                                if !type_text.is_empty()
1216                                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1217                                {
1218                                    scopes[scope_idx].types.insert(name, type_text);
1219                                }
1220                            }
1221                        }
1222                    }
1223                }
1224                continue;
1225            }
1226            scopes[scope_idx].bindings.insert(var_name.clone());
1227
1228            // Check for explicit type
1229            if let Some(type_node) = child.child_by_field_name("type") {
1230                let type_text = extract_base_type(type_node, source);
1231                if !type_text.is_empty()
1232                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1233                {
1234                    scopes[scope_idx]
1235                        .types
1236                        .insert(var_name, type_text);
1237                    continue;
1238                }
1239            }
1240
1241            // Check RHS value
1242            if let Some(value) = child.child_by_field_name("value") {
1243                let rhs = if value.kind() == "expression_list" {
1244                    value.named_child(0).unwrap_or(value)
1245                } else {
1246                    value
1247                };
1248                record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
1249            }
1250        }
1251    }
1252}
1253
1254/// Record type binding from a RHS expression (works for all languages).
1255/// Handles: constructor calls, new expressions, struct literals, function calls.
1256fn record_type_from_rhs(
1257    rhs: tree_sitter::Node,
1258    var_name: &str,
1259    scope_idx: usize,
1260    scopes: &mut Vec<Scope>,
1261    source: &[u8],
1262) {
1263    match rhs.kind() {
1264        // Python/Go: Foo() or func()
1265        "call" | "call_expression" => {
1266            let func_node = rhs
1267                .child_by_field_name("function")
1268                .or_else(|| rhs.named_child(0));
1269            if let Some(func) = func_node {
1270                if func.kind() == "identifier" || func.kind() == "simple_identifier" || func.kind() == "type_identifier" {
1271                    let name = func.utf8_text(source).unwrap_or("");
1272                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1273                        scopes[scope_idx]
1274                            .types
1275                            .insert(var_name.to_string(), name.to_string());
1276                    } else {
1277                        scopes[scope_idx]
1278                            .pending_call_types
1279                            .insert(var_name.to_string(), name.to_string());
1280                    }
1281                }
1282                // Rust: Type::new() / Type::from() etc.
1283                if func.kind() == "scoped_identifier" {
1284                    let text = func.utf8_text(source).unwrap_or("");
1285                    let parts: Vec<&str> = text.split("::").collect();
1286                    if parts.len() >= 2 {
1287                        let type_name = parts[0];
1288                        let method_name = parts[parts.len() - 1];
1289                        if type_name.chars().next().map_or(false, |c| c.is_uppercase()) {
1290                            scopes[scope_idx]
1291                                .types
1292                                .insert(var_name.to_string(), type_name.to_string());
1293                        } else {
1294                            scopes[scope_idx]
1295                                .pending_call_types
1296                                .insert(var_name.to_string(), method_name.to_string());
1297                        }
1298                    }
1299                }
1300                // Go: package.NewFoo() or package.GetFoo()
1301                if func.kind() == "selector_expression" {
1302                    let field = func
1303                        .child_by_field_name("field")
1304                        .and_then(|n| n.utf8_text(source).ok())
1305                        .unwrap_or("");
1306                    // Go convention: NewFoo() returns *Foo
1307                    if let Some(type_name) = field.strip_prefix("New") {
1308                        if !type_name.is_empty()
1309                            && type_name.chars().next().map_or(false, |c| c.is_uppercase())
1310                        {
1311                            scopes[scope_idx]
1312                                .types
1313                                .insert(var_name.to_string(), type_name.to_string());
1314                        }
1315                    } else if field.starts_with("Get") || field.chars().next().map_or(false, |c| c.is_uppercase()) {
1316                        // Other Go package functions: record for return type resolution
1317                        scopes[scope_idx]
1318                            .pending_call_types
1319                            .insert(var_name.to_string(), field.to_string());
1320                    }
1321                }
1322            }
1323        }
1324        // TS: new Foo()
1325        "new_expression" => {
1326            if let Some(constructor) = rhs.child_by_field_name("constructor") {
1327                let name = constructor.utf8_text(source).unwrap_or("");
1328                if !name.is_empty() {
1329                    scopes[scope_idx]
1330                        .types
1331                        .insert(var_name.to_string(), name.to_string());
1332                }
1333            }
1334        }
1335        // Go: Foo{} (composite_literal / struct literal)
1336        "composite_literal" => {
1337            if let Some(type_node) = rhs.child_by_field_name("type") {
1338                let name = type_node.utf8_text(source).unwrap_or("");
1339                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1340                    scopes[scope_idx]
1341                        .types
1342                        .insert(var_name.to_string(), name.to_string());
1343                }
1344            }
1345        }
1346        _ => {}
1347    }
1348}
1349
1350/// Extract the base type name from a type annotation node.
1351/// Strips pointers, references, generics to get just the type name.
1352fn extract_base_type(type_node: tree_sitter::Node, source: &[u8]) -> String {
1353    let text = type_node.utf8_text(source).unwrap_or("").trim().to_string();
1354    // Strip reference/pointer prefixes and mut keyword
1355    let text = text.trim_start_matches('&').trim_start_matches('*');
1356    let text = text.strip_prefix("mut ").unwrap_or(text).trim_start();
1357    // Strip generic parameters (angle brackets and Python-style square brackets)
1358    let text = if let Some(i) = text.find('<') {
1359        &text[..i]
1360    } else if let Some(i) = text.find('[') {
1361        &text[..i]
1362    } else {
1363        text
1364    };
1365    // Strip lifetime annotations for Rust
1366    let text = text.trim();
1367    // For type_annotation nodes in TS, strip the leading `: `
1368    let text = text.trim_start_matches(':').trim();
1369    text.to_string()
1370}
1371
1372/// Parse Go receiver type from method content: `func (r *ReceiverType) Name(...)`
1373pub fn extract_go_receiver_type(content: &str) -> Option<String> {
1374    let after_func = content.strip_prefix("func")?.trim_start();
1375    let paren_start = after_func.find('(')?;
1376    let paren_end = after_func.find(')')?;
1377    let receiver_block = &after_func[paren_start + 1..paren_end];
1378    // Could be: "r ReceiverType", "r *ReceiverType", "*ReceiverType"
1379    let parts: Vec<&str> = receiver_block.split_whitespace().collect();
1380    let type_str = parts.last()?;
1381    let name = type_str.trim_start_matches('*');
1382    if name.is_empty() {
1383        None
1384    } else {
1385        Some(name.to_string())
1386    }
1387}
1388
1389/// Build Go package index: pkg_name → [(entity_name, entity_id)]
1390/// Maps file stems and parent directory names to entities for O(1) package import lookup.
1391fn build_go_pkg_index(
1392    symbol_table: &HashMap<String, Vec<String>>,
1393    entity_map: &HashMap<String, EntityInfo>,
1394) -> HashMap<String, Vec<(String, String)>> {
1395    let mut idx: HashMap<String, Vec<(String, String)>> = HashMap::new();
1396    for (name, target_ids) in symbol_table.iter() {
1397        for target_id in target_ids {
1398            if let Some(entity) = entity_map.get(target_id) {
1399                if !entity.file_path.ends_with(".go") {
1400                    continue;
1401                }
1402                let file_stem = entity.file_path.rsplit('/').next().unwrap_or(&entity.file_path);
1403                let file_stem = file_stem.strip_suffix(".go").unwrap_or(file_stem);
1404                idx.entry(file_stem.to_string())
1405                    .or_default()
1406                    .push((name.clone(), target_id.clone()));
1407                if let Some(parent_start) = entity.file_path.rfind('/') {
1408                    let parent_path = &entity.file_path[..parent_start];
1409                    if let Some(dir_name_start) = parent_path.rfind('/') {
1410                        let dir_name = &parent_path[dir_name_start + 1..];
1411                        if dir_name != file_stem {
1412                            idx.entry(dir_name.to_string())
1413                                .or_default()
1414                                .push((name.clone(), target_id.clone()));
1415                        }
1416                    } else if !parent_path.is_empty() && parent_path != file_stem {
1417                        idx.entry(parent_path.to_string())
1418                            .or_default()
1419                            .push((name.clone(), target_id.clone()));
1420                    }
1421                }
1422            }
1423        }
1424    }
1425    idx
1426}
1427
1428/// Scan function bodies/signatures for return types to build a return type map.
1429fn scan_return_types(
1430    root: tree_sitter::Node,
1431    _file_path: &str,
1432    file_entities: &[&SemanticEntity],
1433    source: &[u8],
1434    return_type_map: &mut HashMap<String, String>,
1435    config: &ScopeResolveConfig,
1436) {
1437    let mut worklist = vec![root];
1438    while let Some(node) = worklist.pop() {
1439        let kind = node.kind();
1440
1441        let is_func = config.function_scope_nodes.contains(&kind);
1442
1443        if is_func {
1444            let func_name = node
1445                .child_by_field_name("name")
1446                .and_then(|n| n.utf8_text(source).ok())
1447                .unwrap_or("");
1448
1449            let func_entity = file_entities.iter().find(|e| {
1450                e.name == func_name && {
1451                    let line = node.start_position().row + 1;
1452                    e.start_line <= line && line <= e.end_line
1453                }
1454            }).copied();
1455
1456            if let Some(fe) = func_entity {
1457                // Try explicit return type annotation first
1458                let ret_type = config.return_type_field.and_then(|field| {
1459                    node.child_by_field_name(field)
1460                        .map(|n| extract_base_type(n, source))
1461                        .filter(|t| !t.is_empty() && t.chars().next().map_or(false, |c| c.is_uppercase()))
1462                });
1463
1464                if let Some(rt) = ret_type {
1465                    return_type_map.insert(fe.id.clone(), rt);
1466                } else {
1467                    // Fall back to body heuristic: return ClassName()
1468                    if let Some(ret_type) = find_return_constructor(node, source) {
1469                        return_type_map.insert(fe.id.clone(), ret_type);
1470                    }
1471                }
1472            }
1473        }
1474
1475        let mut cursor = node.walk();
1476        let children: Vec<_> = node.named_children(&mut cursor).collect();
1477        for child in children.into_iter().rev() {
1478            worklist.push(child);
1479        }
1480    }
1481}
1482
1483/// Find `return ClassName()` patterns in a function body (heuristic fallback).
1484fn find_return_constructor(root: tree_sitter::Node, source: &[u8]) -> Option<String> {
1485    let mut worklist = vec![root];
1486    while let Some(node) = worklist.pop() {
1487        let mut cursor = node.walk();
1488        for child in node.named_children(&mut cursor) {
1489            if child.kind() == "return_statement" {
1490                let mut inner_cursor = child.walk();
1491                for ret_child in child.named_children(&mut inner_cursor) {
1492                    // Python: call, TS/Go: call_expression
1493                    if ret_child.kind() == "call" || ret_child.kind() == "call_expression" {
1494                        if let Some(func) = ret_child.child_by_field_name("function") {
1495                            if func.kind() == "identifier" {
1496                                let name = func.utf8_text(source).unwrap_or("");
1497                                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1498                                    return Some(name.to_string());
1499                                }
1500                            }
1501                        }
1502                    }
1503                    // TS: new ClassName()
1504                    if ret_child.kind() == "new_expression" {
1505                        if let Some(constructor) = ret_child.child_by_field_name("constructor") {
1506                            let name = constructor.utf8_text(source).unwrap_or("");
1507                            if !name.is_empty() {
1508                                return Some(name.to_string());
1509                            }
1510                        }
1511                    }
1512                    // Go: StructName{} (composite_literal)
1513                    if ret_child.kind() == "composite_literal" {
1514                        if let Some(type_node) = ret_child.child_by_field_name("type") {
1515                            let name = type_node.utf8_text(source).unwrap_or("");
1516                            if name.chars().next().map_or(false, |c| c.is_uppercase()) {
1517                                return Some(name.to_string());
1518                            }
1519                        }
1520                    }
1521                }
1522            }
1523            // Recurse into blocks
1524            let ck = child.kind();
1525            if ck == "block" || ck == "statement_block" {
1526                worklist.push(child);
1527            }
1528        }
1529    }
1530    None
1531}
1532
1533/// Scan for instance attribute types: __init__ self.attr patterns (Python/TS),
1534/// struct field declarations (Rust/Go).
1535fn scan_init_self_attrs(
1536    root: tree_sitter::Node,
1537    _file_path: &str,
1538    _file_entities: &[&SemanticEntity],
1539    _entity_map: &HashMap<String, EntityInfo>,
1540    source: &[u8],
1541    instance_attr_types: &mut HashMap<(String, String), String>,
1542    init_params_map: &mut HashMap<String, Vec<String>>,
1543    attr_to_param_map: &mut HashMap<(String, String), String>,
1544    config: &ScopeResolveConfig,
1545) {
1546    let mut worklist = vec![root];
1547    while let Some(node) = worklist.pop() {
1548        let kind = node.kind();
1549
1550        match &config.init_strategy {
1551            InitStrategy::ConstructorBody { class_nodes, init_node_kind, self_keyword: _, .. } => {
1552                if class_nodes.contains(&kind) {
1553                    let class_name = node
1554                        .child_by_field_name("name")
1555                        .and_then(|n| n.utf8_text(source).ok())
1556                        .unwrap_or("")
1557                        .to_string();
1558
1559                    if !class_name.is_empty() {
1560                        // Determine lang for scan_class_for_init using init_node_kind as discriminator
1561                        let lang = match *init_node_kind {
1562                            "function_definition" => "python",
1563                            "method_definition" => "typescript",
1564                            "init_declaration" => "swift",
1565                            "anonymous_initializer" => "kotlin",
1566                            _ => "typescript",
1567                        };
1568                        scan_class_for_init(node, &class_name, source, instance_attr_types, init_params_map, attr_to_param_map, lang);
1569                    }
1570                }
1571            }
1572            InitStrategy::StructFields { struct_nodes } => {
1573                if struct_nodes.contains(&kind) {
1574                    // Rust struct: extract field types directly
1575                    if kind == "struct_item" {
1576                        let struct_name = node
1577                            .child_by_field_name("name")
1578                            .and_then(|n| n.utf8_text(source).ok())
1579                            .unwrap_or("")
1580                            .to_string();
1581
1582                        if !struct_name.is_empty() {
1583                            scan_rust_struct_fields(node, &struct_name, source, instance_attr_types);
1584                        }
1585                    }
1586                    // Go: extract field types from type declarations
1587                    if kind == "type_declaration" {
1588                        scan_go_struct_fields(node, source, instance_attr_types);
1589                    }
1590                }
1591            }
1592            InitStrategy::None => {}
1593        }
1594
1595        let mut cursor = node.walk();
1596        let children: Vec<_> = node.named_children(&mut cursor).collect();
1597        for child in children.into_iter().rev() {
1598            worklist.push(child);
1599        }
1600    }
1601}
1602
1603/// Rust: extract field types from `struct Foo { conn: Connection, ... }`
1604fn scan_rust_struct_fields(
1605    node: tree_sitter::Node,
1606    struct_name: &str,
1607    source: &[u8],
1608    instance_attr_types: &mut HashMap<(String, String), String>,
1609) {
1610    let mut cursor = node.walk();
1611    for child in node.named_children(&mut cursor) {
1612        if child.kind() == "field_declaration_list" {
1613            let mut inner_cursor = child.walk();
1614            for field in child.named_children(&mut inner_cursor) {
1615                if field.kind() == "field_declaration" {
1616                    let field_name = field
1617                        .child_by_field_name("name")
1618                        .and_then(|n| n.utf8_text(source).ok())
1619                        .unwrap_or("");
1620                    let field_type = field
1621                        .child_by_field_name("type")
1622                        .map(|n| extract_base_type(n, source))
1623                        .unwrap_or_default();
1624
1625                    if !field_name.is_empty()
1626                        && !field_type.is_empty()
1627                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1628                    {
1629                        instance_attr_types.insert(
1630                            (struct_name.to_string(), field_name.to_string()),
1631                            field_type,
1632                        );
1633                    }
1634                }
1635            }
1636        }
1637    }
1638}
1639
1640/// Go: extract field types from `type Foo struct { conn Connection; ... }`
1641fn scan_go_struct_fields(
1642    node: tree_sitter::Node,
1643    source: &[u8],
1644    instance_attr_types: &mut HashMap<(String, String), String>,
1645) {
1646    let mut cursor = node.walk();
1647    for child in node.named_children(&mut cursor) {
1648        if child.kind() == "type_spec" {
1649            let struct_name = child
1650                .child_by_field_name("name")
1651                .and_then(|n| n.utf8_text(source).ok())
1652                .unwrap_or("")
1653                .to_string();
1654
1655            if struct_name.is_empty() {
1656                continue;
1657            }
1658
1659            // Look for struct_type child
1660            if let Some(type_node) = child.child_by_field_name("type") {
1661                if type_node.kind() == "struct_type" {
1662                    let mut fields_cursor = type_node.walk();
1663                    for field_list in type_node.named_children(&mut fields_cursor) {
1664                        if field_list.kind() == "field_declaration_list" {
1665                            let mut inner = field_list.walk();
1666                            for field in field_list.named_children(&mut inner) {
1667                                if field.kind() == "field_declaration" {
1668                                    // Go field: name type
1669                                    let field_name = field
1670                                        .child_by_field_name("name")
1671                                        .and_then(|n| n.utf8_text(source).ok())
1672                                        .unwrap_or("");
1673                                    let field_type = field
1674                                        .child_by_field_name("type")
1675                                        .map(|n| extract_base_type(n, source))
1676                                        .unwrap_or_default();
1677
1678                                    if !field_name.is_empty()
1679                                        && !field_type.is_empty()
1680                                        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1681                                    {
1682                                        instance_attr_types.insert(
1683                                            (struct_name.clone(), field_name.to_string()),
1684                                            field_type,
1685                                        );
1686                                    }
1687                                }
1688                            }
1689                        }
1690                    }
1691                }
1692            }
1693        }
1694    }
1695}
1696
1697fn scan_class_for_init(
1698    root: tree_sitter::Node,
1699    class_name: &str,
1700    source: &[u8],
1701    instance_attr_types: &mut HashMap<(String, String), String>,
1702    init_params_map: &mut HashMap<String, Vec<String>>,
1703    attr_to_param_map: &mut HashMap<(String, String), String>,
1704    lang: &str,
1705) {
1706    // Kotlin: extract primary constructor params (class_parameter nodes with val/var)
1707    if lang == "kotlin" {
1708        scan_kotlin_primary_constructor(root, class_name, source, instance_attr_types);
1709    }
1710
1711    let mut worklist = vec![root];
1712    while let Some(node) = worklist.pop() {
1713        let mut cursor = node.walk();
1714        for child in node.named_children(&mut cursor) {
1715            let ck = child.kind();
1716
1717            // Python __init__
1718            if ck == "function_definition" && lang == "python" {
1719                let name = child
1720                    .child_by_field_name("name")
1721                    .and_then(|n| n.utf8_text(source).ok())
1722                    .unwrap_or("");
1723                if name == "__init__" {
1724                    let params = extract_init_params(child, source);
1725                    let ordered_params = extract_init_param_names_ordered(child, source);
1726                    init_params_map.insert(class_name.to_string(), ordered_params);
1727                    scan_init_body(child, class_name, &params, source, instance_attr_types, attr_to_param_map);
1728                }
1729            }
1730
1731            // TS constructor
1732            if ck == "method_definition" && lang == "typescript" {
1733                let name = child
1734                    .child_by_field_name("name")
1735                    .and_then(|n| n.utf8_text(source).ok())
1736                    .unwrap_or("");
1737                if name == "constructor" {
1738                    // Scan for this.attr = param patterns
1739                    scan_ts_constructor_body(child, class_name, source, instance_attr_types, init_params_map, attr_to_param_map);
1740                }
1741            }
1742
1743            // Swift init_declaration
1744            if ck == "init_declaration" && lang == "swift" {
1745                scan_swift_init_body(child, class_name, source, instance_attr_types, init_params_map, attr_to_param_map);
1746            }
1747
1748            // Kotlin anonymous_initializer (init { ... } block)
1749            if ck == "anonymous_initializer" && lang == "kotlin" {
1750                scan_kotlin_init_body(child, class_name, source, instance_attr_types, attr_to_param_map);
1751            }
1752
1753            // TS: typed class field declarations `private conn: Connection`
1754            if (ck == "public_field_definition" || ck == "property_declaration" || ck == "field_definition") && lang == "typescript" {
1755                let field_name = child
1756                    .child_by_field_name("name")
1757                    .and_then(|n| n.utf8_text(source).ok())
1758                    .unwrap_or("");
1759                if let Some(type_ann) = child.child_by_field_name("type") {
1760                    let type_text = extract_base_type(type_ann, source);
1761                    if !field_name.is_empty()
1762                        && !type_text.is_empty()
1763                        && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1764                    {
1765                        instance_attr_types.insert(
1766                            (class_name.to_string(), field_name.to_string()),
1767                            type_text,
1768                        );
1769                    }
1770                }
1771            }
1772
1773            // Swift: typed property declarations `var conn: Connection`
1774            if ck == "property_declaration" && lang == "swift" {
1775                scan_swift_property_declaration(child, class_name, source, instance_attr_types);
1776            }
1777
1778            // Kotlin: typed property declarations `val conn: Connection`
1779            if ck == "property_declaration" && lang == "kotlin" {
1780                scan_kotlin_property_declaration(child, class_name, source, instance_attr_types);
1781            }
1782
1783            if ck == "block" || ck == "class_body" || ck == "statement_block"
1784                || ck == "struct_body" || ck == "function_body" || ck == "code_block"
1785                || ck == "statements" || ck == "enum_class_body"
1786            {
1787                worklist.push(child);
1788            }
1789        }
1790    }
1791}
1792
1793/// Swift: scan init body for `self.attr = param` patterns
1794fn scan_swift_init_body(
1795    node: tree_sitter::Node,
1796    class_name: &str,
1797    source: &[u8],
1798    instance_attr_types: &mut HashMap<(String, String), String>,
1799    init_params_map: &mut HashMap<String, Vec<String>>,
1800    attr_to_param_map: &mut HashMap<(String, String), String>,
1801) {
1802    let params = extract_init_params(node, source);
1803    let ordered_params = extract_init_param_names_ordered(node, source);
1804    init_params_map.insert(class_name.to_string(), ordered_params);
1805
1806    // Walk body looking for self.X = Y
1807    let mut worklist = vec![node];
1808    while let Some(wnode) = worklist.pop() {
1809        let mut cursor = wnode.walk();
1810        for child in wnode.named_children(&mut cursor) {
1811            let ck = child.kind();
1812            // Look for assignment: self.X = Y via directly_assigned_expression or assignment
1813            if ck == "directly_assigned_expression" || ck == "assignment" {
1814                if let Some(left) = child.child_by_field_name("left").or_else(|| child.named_child(0)) {
1815                    if left.kind() == "navigation_expression" {
1816                        let obj = left.child_by_field_name("target")
1817                            .and_then(|n| n.utf8_text(source).ok())
1818                            .unwrap_or("");
1819                        let prop = left.child_by_field_name("suffix")
1820                            .and_then(|n| n.utf8_text(source).ok())
1821                            .unwrap_or("");
1822                        if obj == "self" && !prop.is_empty() {
1823                            if let Some(right) = child.child_by_field_name("right").or_else(|| child.named_child(1)) {
1824                                if right.kind() == "simple_identifier" || right.kind() == "identifier" {
1825                                    let rhs_name = right.utf8_text(source).unwrap_or("");
1826                                    if params.contains_key(rhs_name) {
1827                                        attr_to_param_map.insert(
1828                                            (class_name.to_string(), prop.to_string()),
1829                                            rhs_name.to_string(),
1830                                        );
1831                                        if let Some(Some(type_hint)) = params.get(rhs_name) {
1832                                            instance_attr_types.insert(
1833                                                (class_name.to_string(), prop.to_string()),
1834                                                type_hint.clone(),
1835                                            );
1836                                        }
1837                                    }
1838                                }
1839                            }
1840                        }
1841                    }
1842                }
1843            }
1844            if ck == "function_body" || ck == "code_block" || ck == "statements"
1845                || ck == "expression_statement" || ck == "block"
1846            {
1847                worklist.push(child);
1848            }
1849        }
1850    }
1851}
1852
1853/// Swift: extract typed property declarations `var conn: Connection`
1854fn scan_swift_property_declaration(
1855    node: tree_sitter::Node,
1856    class_name: &str,
1857    source: &[u8],
1858    instance_attr_types: &mut HashMap<(String, String), String>,
1859) {
1860    // Swift property_declaration has a "name" (via pattern binding) and type annotation
1861    let mut cursor = node.walk();
1862    for child in node.named_children(&mut cursor) {
1863        if child.kind() == "pattern" || child.kind() == "simple_identifier" || child.kind() == "identifier" {
1864            let field_name = child.utf8_text(source).unwrap_or("");
1865            if let Some(type_ann) = node.child_by_field_name("type") {
1866                let type_text = extract_base_type(type_ann, source);
1867                if !field_name.is_empty()
1868                    && !type_text.is_empty()
1869                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1870                {
1871                    instance_attr_types.insert(
1872                        (class_name.to_string(), field_name.to_string()),
1873                        type_text,
1874                    );
1875                }
1876            }
1877        }
1878    }
1879}
1880
1881/// Kotlin: extract typed property declarations `val conn: Connection`
1882fn scan_kotlin_property_declaration(
1883    node: tree_sitter::Node,
1884    class_name: &str,
1885    source: &[u8],
1886    instance_attr_types: &mut HashMap<(String, String), String>,
1887) {
1888    let field_name = node
1889        .child_by_field_name("name")
1890        .and_then(|n| n.utf8_text(source).ok())
1891        .unwrap_or("");
1892    let field_type = node
1893        .child_by_field_name("type")
1894        .map(|n| extract_base_type(n, source))
1895        .unwrap_or_default();
1896
1897    if !field_name.is_empty()
1898        && !field_type.is_empty()
1899        && field_type.chars().next().map_or(false, |c| c.is_uppercase())
1900    {
1901        instance_attr_types.insert(
1902            (class_name.to_string(), field_name.to_string()),
1903            field_type,
1904        );
1905    }
1906}
1907
1908/// Kotlin: extract primary constructor params with val/var as instance attributes
1909fn scan_kotlin_primary_constructor(
1910    class_node: tree_sitter::Node,
1911    class_name: &str,
1912    source: &[u8],
1913    instance_attr_types: &mut HashMap<(String, String), String>,
1914) {
1915    // Look for primary_constructor child, then class_parameter nodes
1916    let mut cursor = class_node.walk();
1917    for child in class_node.named_children(&mut cursor) {
1918        if child.kind() == "primary_constructor" {
1919            let mut pc_cursor = child.walk();
1920            for param in child.named_children(&mut pc_cursor) {
1921                if param.kind() == "class_parameter" {
1922                    // Check if this has val/var modifier (makes it a property)
1923                    let text = param.utf8_text(source).unwrap_or("");
1924                    let has_val_var = text.starts_with("val ") || text.starts_with("var ")
1925                        || text.contains("val ") || text.contains("var ");
1926                    if has_val_var {
1927                        let param_name = param
1928                            .child_by_field_name("name")
1929                            .and_then(|n| n.utf8_text(source).ok())
1930                            .unwrap_or("");
1931                        let param_type = param
1932                            .child_by_field_name("type")
1933                            .map(|n| extract_base_type(n, source))
1934                            .unwrap_or_default();
1935                        if !param_name.is_empty()
1936                            && !param_type.is_empty()
1937                            && param_type.chars().next().map_or(false, |c| c.is_uppercase())
1938                        {
1939                            instance_attr_types.insert(
1940                                (class_name.to_string(), param_name.to_string()),
1941                                param_type,
1942                            );
1943                        }
1944                    }
1945                }
1946            }
1947        }
1948    }
1949}
1950
1951/// Kotlin: scan init { ... } body for this.attr = expr patterns
1952fn scan_kotlin_init_body(
1953    node: tree_sitter::Node,
1954    class_name: &str,
1955    source: &[u8],
1956    instance_attr_types: &mut HashMap<(String, String), String>,
1957    attr_to_param_map: &mut HashMap<(String, String), String>,
1958) {
1959    let mut worklist = vec![node];
1960    while let Some(wnode) = worklist.pop() {
1961        let mut cursor = wnode.walk();
1962        for child in wnode.named_children(&mut cursor) {
1963            let ck = child.kind();
1964            if ck == "assignment" || ck == "directly_assigned_expression" {
1965                if let Some(left) = child.child_by_field_name("left").or_else(|| child.named_child(0)) {
1966                    if left.kind() == "navigation_expression" {
1967                        let obj = left.child_by_field_name("expression")
1968                            .and_then(|n| n.utf8_text(source).ok())
1969                            .unwrap_or("");
1970                        let prop = left.child_by_field_name("navigation_suffix")
1971                            .and_then(|n| n.utf8_text(source).ok())
1972                            .unwrap_or("");
1973                        if obj == "this" && !prop.is_empty() {
1974                            if let Some(right) = child.child_by_field_name("right").or_else(|| child.named_child(1)) {
1975                                if right.kind() == "simple_identifier" || right.kind() == "identifier" {
1976                                    let rhs_name = right.utf8_text(source).unwrap_or("");
1977                                    attr_to_param_map.insert(
1978                                        (class_name.to_string(), prop.to_string()),
1979                                        rhs_name.to_string(),
1980                                    );
1981                                }
1982                                // If RHS is a constructor call, record type directly
1983                                if right.kind() == "call_expression" {
1984                                    let callee = right.child_by_field_name("function")
1985                                        .and_then(|n| n.utf8_text(source).ok())
1986                                        .unwrap_or("");
1987                                    if !callee.is_empty()
1988                                        && callee.chars().next().map_or(false, |c| c.is_uppercase())
1989                                    {
1990                                        instance_attr_types.insert(
1991                                            (class_name.to_string(), prop.to_string()),
1992                                            callee.to_string(),
1993                                        );
1994                                    }
1995                                }
1996                            }
1997                        }
1998                    }
1999                }
2000            }
2001            if ck == "statements" || ck == "block" || ck == "expression_statement" {
2002                worklist.push(child);
2003            }
2004        }
2005    }
2006}
2007
2008/// TS: scan constructor body for `this.attr = param` patterns
2009fn scan_ts_constructor_body(
2010    node: tree_sitter::Node,
2011    class_name: &str,
2012    source: &[u8],
2013    instance_attr_types: &mut HashMap<(String, String), String>,
2014    init_params_map: &mut HashMap<String, Vec<String>>,
2015    attr_to_param_map: &mut HashMap<(String, String), String>,
2016) {
2017    // Extract constructor params
2018    let params = extract_init_params(node, source);
2019    let ordered_params = extract_init_param_names_ordered(node, source);
2020    init_params_map.insert(class_name.to_string(), ordered_params);
2021
2022    // Scan body for this.X = param
2023    scan_init_body_this(node, class_name, &params, source, instance_attr_types, attr_to_param_map);
2024}
2025
2026/// Scan constructor body for `this.attr = param` patterns (TS variant)
2027fn scan_init_body_this(
2028    root: tree_sitter::Node,
2029    class_name: &str,
2030    params: &HashMap<String, Option<String>>,
2031    source: &[u8],
2032    instance_attr_types: &mut HashMap<(String, String), String>,
2033    attr_to_param_map: &mut HashMap<(String, String), String>,
2034) {
2035    let mut worklist = vec![root];
2036    while let Some(node) = worklist.pop() {
2037        let mut cursor = node.walk();
2038        for child in node.named_children(&mut cursor) {
2039            let ck = child.kind();
2040            if ck == "expression_statement" {
2041                // Look for assignment: this.X = Y
2042                let mut inner_cursor = child.walk();
2043                for inner in child.named_children(&mut inner_cursor) {
2044                    if inner.kind() == "assignment_expression" {
2045                        if let Some(left) = inner.child_by_field_name("left") {
2046                            if left.kind() == "member_expression" {
2047                                let obj = left.child_by_field_name("object")
2048                                    .and_then(|n| n.utf8_text(source).ok())
2049                                    .unwrap_or("");
2050                                let prop = left.child_by_field_name("property")
2051                                    .and_then(|n| n.utf8_text(source).ok())
2052                                    .unwrap_or("");
2053                                if obj == "this" && !prop.is_empty() {
2054                                    if let Some(right) = inner.child_by_field_name("right") {
2055                                        if right.kind() == "identifier" {
2056                                            let rhs_name = right.utf8_text(source).unwrap_or("");
2057                                            if params.contains_key(rhs_name) {
2058                                                attr_to_param_map.insert(
2059                                                    (class_name.to_string(), prop.to_string()),
2060                                                    rhs_name.to_string(),
2061                                                );
2062                                                if let Some(Some(type_hint)) = params.get(rhs_name) {
2063                                                    instance_attr_types.insert(
2064                                                        (class_name.to_string(), prop.to_string()),
2065                                                        type_hint.clone(),
2066                                                    );
2067                                                }
2068                                            }
2069                                        }
2070                                        if right.kind() == "new_expression" {
2071                                            if let Some(ctor) = right.child_by_field_name("constructor") {
2072                                                let name = ctor.utf8_text(source).unwrap_or("");
2073                                                if !name.is_empty() {
2074                                                    instance_attr_types.insert(
2075                                                        (class_name.to_string(), prop.to_string()),
2076                                                        name.to_string(),
2077                                                    );
2078                                                }
2079                                            }
2080                                        }
2081                                    }
2082                                }
2083                            }
2084                        }
2085                    }
2086                }
2087            }
2088            if ck == "statement_block" || ck == "block" {
2089                worklist.push(child);
2090            }
2091        }
2092    }
2093}
2094
2095/// Extract __init__ parameter names in order (excluding self).
2096fn extract_init_param_names_ordered(func_node: tree_sitter::Node, source: &[u8]) -> Vec<String> {
2097    let mut names = Vec::new();
2098    if let Some(params_node) = func_node.child_by_field_name("parameters") {
2099        let mut cursor = params_node.walk();
2100        for child in params_node.named_children(&mut cursor) {
2101            let param_name = if child.kind() == "identifier" {
2102                child.utf8_text(source).unwrap_or("").to_string()
2103            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
2104                child.child_by_field_name("name")
2105                    .or_else(|| child.named_child(0))
2106                    .and_then(|n| n.utf8_text(source).ok())
2107                    .unwrap_or("")
2108                    .to_string()
2109            } else {
2110                continue;
2111            };
2112            if param_name != "self" && param_name != "cls" && !param_name.is_empty() {
2113                names.push(param_name);
2114            }
2115        }
2116    }
2117    names
2118}
2119
2120fn extract_init_params(func_node: tree_sitter::Node, source: &[u8]) -> HashMap<String, Option<String>> {
2121    let mut params = HashMap::new();
2122    if let Some(params_node) = func_node.child_by_field_name("parameters") {
2123        let mut cursor = params_node.walk();
2124        for child in params_node.named_children(&mut cursor) {
2125            let param_name = if child.kind() == "identifier" {
2126                child.utf8_text(source).unwrap_or("").to_string()
2127            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter" {
2128                child.child_by_field_name("name")
2129                    .or_else(|| child.named_child(0))
2130                    .and_then(|n| n.utf8_text(source).ok())
2131                    .unwrap_or("")
2132                    .to_string()
2133            } else {
2134                continue;
2135            };
2136            if param_name != "self" && param_name != "cls" {
2137                // Check for type annotation
2138                let type_hint = child.child_by_field_name("type")
2139                    .and_then(|n| n.utf8_text(source).ok())
2140                    .map(|s| s.to_string());
2141                params.insert(param_name, type_hint);
2142            }
2143        }
2144    }
2145    params
2146}
2147
2148fn scan_init_body(
2149    root: tree_sitter::Node,
2150    class_name: &str,
2151    params: &HashMap<String, Option<String>>,
2152    source: &[u8],
2153    instance_attr_types: &mut HashMap<(String, String), String>,
2154    attr_to_param_map: &mut HashMap<(String, String), String>,
2155) {
2156    let mut worklist = vec![root];
2157    while let Some(node) = worklist.pop() {
2158        let mut cursor = node.walk();
2159        for child in node.named_children(&mut cursor) {
2160            if child.kind() == "expression_statement" || child.kind() == "assignment" {
2161                let assign = if child.kind() == "assignment" {
2162                    child
2163                } else {
2164                    let mut inner_cursor = child.walk();
2165                    let children: Vec<_> = child.named_children(&mut inner_cursor).collect();
2166                    match children.into_iter().find(|c| c.kind() == "assignment") {
2167                        Some(a) => a,
2168                        None => continue,
2169                    }
2170                };
2171
2172                if let Some(left) = assign.child_by_field_name("left") {
2173                    if left.kind() == "attribute" {
2174                        let obj = left.child_by_field_name("object")
2175                            .and_then(|n| n.utf8_text(source).ok())
2176                            .unwrap_or("");
2177                        let attr = left.child_by_field_name("attribute")
2178                            .and_then(|n| n.utf8_text(source).ok())
2179                            .unwrap_or("");
2180
2181                        if obj == "self" && !attr.is_empty() {
2182                            if let Some(right) = assign.child_by_field_name("right") {
2183                                if right.kind() == "identifier" {
2184                                    let rhs_name = right.utf8_text(source).unwrap_or("");
2185                                    // Record attr -> param mapping for later inference
2186                                    if params.contains_key(rhs_name) {
2187                                        attr_to_param_map.insert(
2188                                            (class_name.to_string(), attr.to_string()),
2189                                            rhs_name.to_string(),
2190                                        );
2191                                    }
2192                                    // If param has type hint, directly set the type
2193                                    if let Some(Some(type_hint)) = params.get(rhs_name) {
2194                                        instance_attr_types.insert(
2195                                            (class_name.to_string(), attr.to_string()),
2196                                            type_hint.clone(),
2197                                        );
2198                                    }
2199                                }
2200                                if right.kind() == "call" {
2201                                    if let Some(func) = right.child_by_field_name("function") {
2202                                        if func.kind() == "identifier" {
2203                                            let fname = func.utf8_text(source).unwrap_or("");
2204                                            if fname.chars().next().map_or(false, |c| c.is_uppercase()) {
2205                                                instance_attr_types.insert(
2206                                                    (class_name.to_string(), attr.to_string()),
2207                                                    fname.to_string(),
2208                                                );
2209                                            }
2210                                        }
2211                                    }
2212                                }
2213                            }
2214                        }
2215                    }
2216                }
2217            }
2218            if child.kind() == "block" {
2219                worklist.push(child);
2220            }
2221        }
2222    }
2223}
2224
2225/// Infer constructor parameter types by analyzing call sites across all files.
2226/// For `Transaction(get_connection())`, we know get_connection() returns Connection,
2227/// so Transaction.__init__'s conn param has type Connection,
2228/// and self.conn in Transaction has type Connection.
2229fn infer_constructor_param_types(
2230    parsed_files: &[(String, String, tree_sitter::Tree)],
2231    return_type_map: &HashMap<String, String>,
2232    init_params: &HashMap<String, Vec<String>>,
2233    attr_to_param: &HashMap<(String, String), String>,
2234    _symbol_table: &HashMap<String, Vec<String>>,
2235    entity_map: &HashMap<String, EntityInfo>,
2236    instance_attr_types: &mut HashMap<(String, String), String>,
2237) {
2238    // Build func_name -> return_type lookup for quick access
2239    let mut func_name_returns: HashMap<String, String> = HashMap::new();
2240    for (eid, ret_type) in return_type_map {
2241        if let Some(info) = entity_map.get(eid) {
2242            func_name_returns.insert(info.name.clone(), ret_type.clone());
2243        }
2244    }
2245
2246    // Scan all files for constructor call sites: ClassName(arg1, arg2, ...)
2247    // Parallelized: each file produces local results, then merged.
2248    let local_results: Vec<HashMap<(String, String), String>> = maybe_par_iter!(parsed_files)
2249        .map(|(_file_path, content, tree)| {
2250            let source = content.as_bytes();
2251            let mut local_attr_types: HashMap<(String, String), String> = HashMap::new();
2252            scan_constructor_calls(
2253                tree.root_node(),
2254                source,
2255                &func_name_returns,
2256                init_params,
2257                attr_to_param,
2258                &mut local_attr_types,
2259            );
2260            local_attr_types
2261        })
2262        .collect();
2263
2264    for local in local_results {
2265        for (key, val) in local {
2266            instance_attr_types.entry(key).or_insert(val);
2267        }
2268    }
2269}
2270
2271fn scan_constructor_calls(
2272    root: tree_sitter::Node,
2273    source: &[u8],
2274    func_name_returns: &HashMap<String, String>,
2275    init_params: &HashMap<String, Vec<String>>,
2276    attr_to_param: &HashMap<(String, String), String>,
2277    instance_attr_types: &mut HashMap<(String, String), String>,
2278) {
2279    let mut worklist = vec![root];
2280    while let Some(node) = worklist.pop() {
2281        let kind = node.kind();
2282
2283        if kind == "call" {
2284            if let Some(func) = node.child_by_field_name("function") {
2285                if func.kind() == "identifier" {
2286                    let class_name = func.utf8_text(source).unwrap_or("");
2287                    // Only process uppercase names (constructor calls)
2288                    if class_name.chars().next().map_or(false, |c| c.is_uppercase()) {
2289                        if let Some(param_names) = init_params.get(class_name) {
2290                            // Extract argument types
2291                            if let Some(args_node) = node.child_by_field_name("arguments") {
2292                                let mut arg_idx = 0;
2293                                let mut args_cursor = args_node.walk();
2294                                for arg in args_node.named_children(&mut args_cursor) {
2295                                    if arg_idx >= param_names.len() {
2296                                        break;
2297                                    }
2298                                    let param_name = &param_names[arg_idx];
2299
2300                                    // Try to infer the argument's type
2301                                    let arg_type = infer_expr_type(arg, source, func_name_returns);
2302
2303                                    if let Some(at) = arg_type {
2304                                        // Check if any self.attr maps to this param
2305                                        for ((cn, attr), pn) in attr_to_param.iter() {
2306                                            if cn == class_name && pn == param_name {
2307                                                instance_attr_types
2308                                                    .entry((cn.clone(), attr.clone()))
2309                                                    .or_insert(at.clone());
2310                                            }
2311                                        }
2312                                    }
2313
2314                                    arg_idx += 1;
2315                                }
2316                            }
2317                        }
2318                    }
2319                }
2320            }
2321        }
2322
2323        let mut cursor = node.walk();
2324        let children: Vec<_> = node.named_children(&mut cursor).collect();
2325        for child in children.into_iter().rev() {
2326            worklist.push(child);
2327        }
2328    }
2329}
2330
2331/// Infer the type of an expression node.
2332fn infer_expr_type(
2333    node: tree_sitter::Node,
2334    source: &[u8],
2335    func_name_returns: &HashMap<String, String>,
2336) -> Option<String> {
2337    match node.kind() {
2338        "call" => {
2339            if let Some(func) = node.child_by_field_name("function") {
2340                if func.kind() == "identifier" {
2341                    let name = func.utf8_text(source).unwrap_or("");
2342                    // Constructor call: Foo() -> type is Foo
2343                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
2344                        return Some(name.to_string());
2345                    }
2346                    // Function call with known return type
2347                    if let Some(ret) = func_name_returns.get(name) {
2348                        return Some(ret.clone());
2349                    }
2350                }
2351            }
2352            None
2353        }
2354        "identifier" => {
2355            // Could be a variable, but we don't have scope info here
2356            None
2357        }
2358        _ => None,
2359    }
2360}
2361
2362/// Resolve pending call types using the return type map.
2363/// For scopes with `x = func()` where func has a known return type, bind x to that type.
2364fn inject_return_type_bindings(
2365    _entity_inner_scope: &HashMap<String, usize>,
2366    scopes: &mut Vec<Scope>,
2367    return_type_map: &HashMap<String, String>,
2368    import_table: &HashMap<(String, String), String>,
2369    file_path: &str,
2370    entity_map: &HashMap<String, EntityInfo>,
2371) {
2372    // Build function name -> return type lookup
2373    let mut func_name_return_types: HashMap<String, String> = HashMap::new();
2374    for (eid, ret_type) in return_type_map {
2375        if let Some(info) = entity_map.get(eid) {
2376            func_name_return_types.insert(info.name.clone(), ret_type.clone());
2377        }
2378    }
2379
2380    // Also resolve through imports: if `get_connection` is imported and has a known return type
2381    for ((fp, local_name), target_id) in import_table {
2382        if fp == file_path {
2383            if let Some(ret_type) = return_type_map.get(target_id) {
2384                func_name_return_types.insert(local_name.clone(), ret_type.clone());
2385            }
2386        }
2387    }
2388
2389    // Resolve pending call types in all scopes
2390    for scope in scopes.iter_mut() {
2391        let resolved: Vec<(String, String)> = scope
2392            .pending_call_types
2393            .iter()
2394            .filter_map(|(var_name, func_name)| {
2395                func_name_return_types
2396                    .get(func_name)
2397                    .map(|ret_type| (var_name.clone(), ret_type.clone()))
2398            })
2399            .collect();
2400
2401        for (var_name, ret_type) in resolved {
2402            scope.types.insert(var_name, ret_type);
2403        }
2404    }
2405}
2406
2407/// Extract import statements from the AST.
2408fn extract_imports_from_ast(
2409    root: tree_sitter::Node,
2410    file_path: &str,
2411    source: &[u8],
2412    symbol_table: &HashMap<String, Vec<String>>,
2413    entity_map: &HashMap<String, EntityInfo>,
2414    import_table: &mut HashMap<(String, String), String>,
2415    scopes: &mut Vec<Scope>,
2416    config: &ScopeResolveConfig,
2417    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2418) {
2419    let mut worklist = vec![root];
2420    while let Some(node) = worklist.pop() {
2421        let mut cursor = node.walk();
2422        for child in node.named_children(&mut cursor) {
2423            let ck = child.kind();
2424            let handled = match ck {
2425                "import_from_statement" => {
2426                    extract_python_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2427                    true
2428                }
2429                "import_statement" if config.self_keywords.contains(&"self") && config.self_keywords.contains(&"cls") => {
2430                    // Python: `import mod` or `import mod as m`
2431                    extract_python_module_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2432                    true
2433                }
2434                "import_statement" if !config.self_keywords.contains(&"cls") => {
2435                    // TS import_statement (not Python - Python uses import_from_statement)
2436                    extract_ts_import(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2437                    true
2438                }
2439                "use_declaration" => {
2440                    extract_rust_use(child, file_path, source, symbol_table, entity_map, import_table, scopes);
2441                    true
2442                }
2443                "import_declaration" => {
2444                    extract_go_import(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2445                    true
2446                }
2447                _ => false,
2448            };
2449            if !handled {
2450                worklist.push(child);
2451            }
2452        }
2453    }
2454}
2455
2456/// TS: `import { Foo, Bar } from './module'` or `import Foo from './module'`
2457fn extract_ts_import(
2458    node: tree_sitter::Node,
2459    file_path: &str,
2460    source: &[u8],
2461    symbol_table: &HashMap<String, Vec<String>>,
2462    entity_map: &HashMap<String, EntityInfo>,
2463    import_table: &mut HashMap<(String, String), String>,
2464    scopes: &mut Vec<Scope>,
2465) {
2466    // Extract the source module from the `from '...'` clause
2467    let source_path = node
2468        .child_by_field_name("source")
2469        .and_then(|n| n.utf8_text(source).ok())
2470        .unwrap_or("")
2471        .trim_matches(|c: char| c == '\'' || c == '"');
2472
2473    if source_path.is_empty() {
2474        return;
2475    }
2476
2477    // Walk children to find import clause
2478    let mut cursor = node.walk();
2479    for child in node.named_children(&mut cursor) {
2480        if child.kind() == "import_clause" {
2481            let mut clause_cursor = child.walk();
2482            for clause_child in child.named_children(&mut clause_cursor) {
2483                if clause_child.kind() == "named_imports" {
2484                    // { Foo, Bar as Baz }
2485                    let mut imports_cursor = clause_child.walk();
2486                    for spec in clause_child.named_children(&mut imports_cursor) {
2487                        if spec.kind() == "import_specifier" {
2488                            let original = spec
2489                                .child_by_field_name("name")
2490                                .and_then(|n| n.utf8_text(source).ok())
2491                                .unwrap_or("");
2492                            let local = spec
2493                                .child_by_field_name("alias")
2494                                .and_then(|n| n.utf8_text(source).ok())
2495                                .unwrap_or(original);
2496
2497                            if !original.is_empty() {
2498                                resolve_import_name(original, local, source_path, file_path, &[".ts", ".tsx", ".js", ".jsx"], symbol_table, entity_map, import_table, scopes);
2499                            }
2500                        }
2501                    }
2502                } else if clause_child.kind() == "namespace_import" {
2503                    // import * as m from './module'
2504                    // Register all entities from source module so m.foo() resolves
2505                    let mut ns_cursor = clause_child.walk();
2506                    let alias = clause_child
2507                        .child_by_field_name("alias")
2508                        .or_else(|| {
2509                            clause_child.named_children(&mut ns_cursor)
2510                                .find(|c| c.kind() == "identifier")
2511                        })
2512                        .and_then(|n| n.utf8_text(source).ok())
2513                        .unwrap_or("");
2514                    if !alias.is_empty() {
2515                        register_namespace_import(alias, source_path, file_path, &[".ts", ".tsx", ".js", ".jsx"], symbol_table, entity_map, import_table, scopes);
2516                    }
2517                } else if clause_child.kind() == "identifier" {
2518                    // Default import: import Foo from './module'
2519                    let name = clause_child.utf8_text(source).unwrap_or("");
2520                    if !name.is_empty() {
2521                        resolve_import_name(name, name, source_path, file_path, &[".ts", ".tsx", ".js", ".jsx"], symbol_table, entity_map, import_table, scopes);
2522                    }
2523                }
2524            }
2525        }
2526    }
2527}
2528
2529/// Rust: `use crate::module::Name;` or `use crate::module::{A, B};`
2530/// Parse from the text of the use_declaration for reliability.
2531fn extract_rust_use(
2532    node: tree_sitter::Node,
2533    file_path: &str,
2534    source: &[u8],
2535    symbol_table: &HashMap<String, Vec<String>>,
2536    entity_map: &HashMap<String, EntityInfo>,
2537    import_table: &mut HashMap<(String, String), String>,
2538    scopes: &mut Vec<Scope>,
2539) {
2540    let text = node.utf8_text(source).unwrap_or("").trim().to_string();
2541    // Strip `use ` prefix and trailing `;`
2542    let text = text.strip_prefix("use ").unwrap_or(&text);
2543    let text = text.strip_prefix("pub use ").unwrap_or(text);
2544    let text = text.trim_end_matches(';').trim();
2545
2546    // Strip crate/super/self prefix
2547    let text = text
2548        .strip_prefix("crate::")
2549        .or_else(|| text.strip_prefix("super::"))
2550        .or_else(|| text.strip_prefix("self::"))
2551        .unwrap_or(text);
2552
2553    // Check for grouped import: module::{A, B, C}
2554    if let Some(brace_pos) = text.find("::{") {
2555        let module_path = &text[..brace_pos];
2556        let source_module = module_path.rsplit("::").next().unwrap_or(module_path);
2557
2558        let names_part = &text[brace_pos + 3..];
2559        let names_part = names_part.trim_end_matches('}');
2560
2561        for name_part in names_part.split(',') {
2562            let name_part = name_part.trim();
2563            if name_part.is_empty() {
2564                continue;
2565            }
2566            let (original, local) = if let Some(pos) = name_part.find(" as ") {
2567                (name_part[..pos].trim(), name_part[pos + 4..].trim())
2568            } else {
2569                (name_part, name_part)
2570            };
2571            if !original.is_empty() {
2572                resolve_import_name(original, local, source_module, file_path, &[".rs"], symbol_table, entity_map, import_table, scopes);
2573            }
2574        }
2575    } else {
2576        // Simple import: module::Name
2577        let parts: Vec<&str> = text.split("::").collect();
2578        if parts.is_empty() {
2579            return;
2580        }
2581        let imported_name = parts.last().unwrap().trim();
2582        let (original, local) = if let Some(pos) = imported_name.find(" as ") {
2583            (&imported_name[..pos], imported_name[pos + 4..].trim())
2584        } else {
2585            (imported_name, imported_name)
2586        };
2587        let source_module = if parts.len() >= 2 {
2588            parts[parts.len() - 2]
2589        } else {
2590            parts[0]
2591        };
2592        if !original.is_empty() && !source_module.is_empty() {
2593            resolve_import_name(original, local, source_module, file_path, &[".rs"], symbol_table, entity_map, import_table, scopes);
2594        }
2595    }
2596}
2597
2598/// Go: `import ("module/path")` - maps package names to entities
2599fn extract_go_import(
2600    node: tree_sitter::Node,
2601    file_path: &str,
2602    source: &[u8],
2603    symbol_table: &HashMap<String, Vec<String>>,
2604    entity_map: &HashMap<String, EntityInfo>,
2605    import_table: &mut HashMap<(String, String), String>,
2606    scopes: &mut Vec<Scope>,
2607    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2608) {
2609    let mut cursor = node.walk();
2610    for child in node.named_children(&mut cursor) {
2611        if child.kind() == "import_spec" || child.kind() == "import_spec_list" {
2612            extract_go_import_specs(child, file_path, source, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2613        } else if child.kind() == "interpreted_string_literal" || child.kind() == "raw_string_literal" {
2614            let path = child.utf8_text(source).unwrap_or("")
2615                .trim_matches('"').trim_matches('`');
2616            let pkg_name = path.rsplit('/').next().unwrap_or(path);
2617            register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2618        }
2619    }
2620}
2621
2622fn extract_go_import_specs(
2623    root: tree_sitter::Node,
2624    file_path: &str,
2625    source: &[u8],
2626    symbol_table: &HashMap<String, Vec<String>>,
2627    entity_map: &HashMap<String, EntityInfo>,
2628    import_table: &mut HashMap<(String, String), String>,
2629    scopes: &mut Vec<Scope>,
2630    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2631) {
2632    let mut worklist = vec![root];
2633    while let Some(node) = worklist.pop() {
2634        let mut cursor = node.walk();
2635        for child in node.named_children(&mut cursor) {
2636            if child.kind() == "import_spec" {
2637                let path_node = child.child_by_field_name("path")
2638                    .or_else(|| child.named_child(0));
2639                if let Some(pn) = path_node {
2640                    let path = pn.utf8_text(source).unwrap_or("")
2641                        .trim_matches('"').trim_matches('`');
2642                    let pkg_name = path.rsplit('/').next().unwrap_or(path);
2643                    register_go_package_imports(pkg_name, file_path, symbol_table, entity_map, import_table, scopes, go_pkg_index);
2644                }
2645            } else {
2646                worklist.push(child);
2647            }
2648        }
2649    }
2650}
2651
2652fn register_go_package_imports(
2653    pkg_name: &str,
2654    file_path: &str,
2655    _symbol_table: &HashMap<String, Vec<String>>,
2656    _entity_map: &HashMap<String, EntityInfo>,
2657    import_table: &mut HashMap<(String, String), String>,
2658    scopes: &mut Vec<Scope>,
2659    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
2660) {
2661    // Use pre-built package index for O(1) lookup instead of O(symbol_table) scan
2662    if let Some(entries) = go_pkg_index.get(pkg_name) {
2663        for (name, target_id) in entries {
2664            import_table.insert(
2665                (file_path.to_string(), name.clone()),
2666                target_id.clone(),
2667            );
2668            if !scopes.is_empty() {
2669                scopes[0].defs.insert(name.clone(), target_id.clone());
2670            }
2671        }
2672    }
2673}
2674
2675/// Shared helper: resolve an imported name against the symbol table
2676fn resolve_import_name(
2677    original_name: &str,
2678    local_name: &str,
2679    source_path: &str,
2680    file_path: &str,
2681    extensions: &[&str],
2682    symbol_table: &HashMap<String, Vec<String>>,
2683    entity_map: &HashMap<String, EntityInfo>,
2684    import_table: &mut HashMap<(String, String), String>,
2685    scopes: &mut Vec<Scope>,
2686) {
2687    if let Some(target_ids) = symbol_table.get(original_name) {
2688        let target = find_import_target(
2689            target_ids,
2690            source_path,
2691            file_path,
2692            extensions,
2693            entity_map,
2694        );
2695
2696        if let Some(target_id) = target {
2697            import_table.insert(
2698                (file_path.to_string(), local_name.to_string()),
2699                target_id.clone(),
2700            );
2701            if !scopes.is_empty() {
2702                scopes[0]
2703                    .defs
2704                    .insert(local_name.to_string(), target_id.clone());
2705            }
2706        }
2707    }
2708}
2709
2710/// Register all entities from a source module under a namespace alias.
2711/// For `import * as m from './module'`, all entities from the module
2712/// are registered so that `m.foo()` resolves via the method call path.
2713fn register_namespace_import(
2714    alias: &str,
2715    source_path: &str,
2716    file_path: &str,
2717    extensions: &[&str],
2718    symbol_table: &HashMap<String, Vec<String>>,
2719    entity_map: &HashMap<String, EntityInfo>,
2720    import_table: &mut HashMap<(String, String), String>,
2721    _scopes: &mut Vec<Scope>,
2722) {
2723    // Find all top-level entities whose file matches the imported module.
2724    for (name, target_ids) in symbol_table {
2725        for target_id in target_ids {
2726            if let Some(info) = entity_map.get(target_id) {
2727                if import_source_matches_file(file_path, source_path, extensions, &info.file_path)
2728                    && info.parent_id.is_none() {
2729                    let qualified_name = format!("{alias}.{name}");
2730                    import_table.insert(
2731                        (file_path.to_string(), qualified_name.clone()),
2732                        target_id.clone(),
2733                    );
2734                }
2735            }
2736        }
2737    }
2738}
2739
2740fn extract_python_import(
2741    node: tree_sitter::Node,
2742    file_path: &str,
2743    source: &[u8],
2744    symbol_table: &HashMap<String, Vec<String>>,
2745    entity_map: &HashMap<String, EntityInfo>,
2746    import_table: &mut HashMap<(String, String), String>,
2747    scopes: &mut Vec<Scope>,
2748) {
2749    // import_from_statement has:
2750    //   module_name (dotted_name or relative_import)
2751    //   name fields (imported names)
2752    let module_node = node.child_by_field_name("module_name");
2753    let module_name = module_node
2754        .and_then(|n| n.utf8_text(source).ok())
2755        .unwrap_or("");
2756
2757    // Walk children to find imported names
2758    let mut cursor = node.walk();
2759    for child in node.named_children(&mut cursor) {
2760        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
2761            let (original, local) = if child.kind() == "aliased_import" {
2762                let orig = child
2763                    .child_by_field_name("name")
2764                    .and_then(|n| n.utf8_text(source).ok())
2765                    .unwrap_or("");
2766                let alias = child
2767                    .child_by_field_name("alias")
2768                    .and_then(|n| n.utf8_text(source).ok())
2769                    .unwrap_or(orig);
2770                (orig, alias)
2771            } else {
2772                let name = child.utf8_text(source).unwrap_or("");
2773                (name, name)
2774            };
2775
2776            if original.is_empty() {
2777                continue;
2778            }
2779
2780            resolve_import_name(original, local, module_name, file_path, &[".py"], symbol_table, entity_map, import_table, scopes);
2781        }
2782    }
2783}
2784
2785/// Python: `import mod` or `import mod as m` — registers all entities from
2786/// the module so that `m.foo()` resolves via the method-call path.
2787fn extract_python_module_import(
2788    node: tree_sitter::Node,
2789    file_path: &str,
2790    source: &[u8],
2791    symbol_table: &HashMap<String, Vec<String>>,
2792    entity_map: &HashMap<String, EntityInfo>,
2793    import_table: &mut HashMap<(String, String), String>,
2794    scopes: &mut Vec<Scope>,
2795) {
2796    let mut cursor = node.walk();
2797    for child in node.named_children(&mut cursor) {
2798        let (module_name, _alias) = match child.kind() {
2799            "dotted_name" => {
2800                let name = child.utf8_text(source).unwrap_or("");
2801                (name, name)
2802            }
2803            "aliased_import" => {
2804                let orig = child
2805                    .child_by_field_name("name")
2806                    .and_then(|n| n.utf8_text(source).ok())
2807                    .unwrap_or("");
2808                let alias = child
2809                    .child_by_field_name("alias")
2810                    .and_then(|n| n.utf8_text(source).ok())
2811                    .unwrap_or(orig);
2812                (orig, alias)
2813            }
2814            _ => continue,
2815        };
2816
2817        if module_name.is_empty() {
2818            continue;
2819        }
2820
2821        // Register all entities from the source module
2822        register_namespace_import(
2823            _alias,
2824            module_name,
2825            file_path,
2826            &[".py"],
2827            symbol_table,
2828            entity_map,
2829            import_table,
2830            scopes,
2831        );
2832    }
2833}
2834
2835/// Collect ALL AST references in a file with a single tree walk.
2836/// Each ref records its row so callers can bucket refs into entities by line range.
2837fn collect_all_file_refs(
2838    root: tree_sitter::Node,
2839    source: &[u8],
2840    config: &ScopeResolveConfig,
2841) -> Vec<AstRef> {
2842    let mut refs = Vec::new();
2843    let mut worklist = vec![root];
2844    while let Some(node) = worklist.pop() {
2845        let node_row = node.start_position().row;
2846        let kind = node.kind();
2847
2848        // Call nodes (e.g. "call", "call_expression", "method_invocation")
2849        if config.call_nodes.contains(&kind) {
2850            match &config.call_style {
2851                CallNodeStyle::FunctionField(field) => {
2852                    if let Some(func) = node.child_by_field_name(field) {
2853                        // Pass empty entity_name — self-ref filtering is done at resolution time
2854                        extract_call_ref(func, "", "", source, &mut refs, config, node_row);
2855                    }
2856                }
2857                CallNodeStyle::FirstChild => {
2858                    // Swift/Kotlin: callee is the first named child (identifier or navigation_expression)
2859                    if let Some(func) = node.named_child(0) {
2860                        extract_call_ref(func, "", "", source, &mut refs, config, node_row);
2861                    }
2862                }
2863                CallNodeStyle::DirectMethod { object_field, method_field } => {
2864                    let method_name = node.child_by_field_name(method_field)
2865                        .and_then(|n| n.utf8_text(source).ok())
2866                        .unwrap_or("");
2867                    if !method_name.is_empty() && !is_builtin(method_name, config) {
2868                        if let Some(obj_node) = node.child_by_field_name(object_field) {
2869                            let receiver = obj_node.utf8_text(source).unwrap_or("").to_string();
2870                            let receiver = receiver.trim_end_matches('.').to_string();
2871                            refs.push(AstRef {
2872                                kind: AstRefKind::MethodCall { receiver, method: method_name.to_string() },
2873                                row: node_row,
2874                            });
2875                        } else {
2876                            refs.push(AstRef {
2877                                kind: AstRefKind::Call(method_name.to_string()),
2878                                row: node_row,
2879                            });
2880                        }
2881                    }
2882                }
2883            }
2884            let mut cursor = node.walk();
2885            let children: Vec<_> = node.named_children(&mut cursor).collect();
2886            for child in children.into_iter().rev() {
2887                worklist.push(child);
2888            }
2889            continue;
2890        }
2891
2892        // Macro invocations (Rust: macro_invocation, macro name in "macro" field)
2893        if kind == "macro_invocation" {
2894            if let Some(macro_node) = node.child_by_field_name("macro") {
2895                let macro_name = macro_node.utf8_text(source).unwrap_or("");
2896                if !macro_name.is_empty() && !is_builtin(macro_name, config) {
2897                    refs.push(AstRef {
2898                        kind: AstRefKind::Call(macro_name.to_string()),
2899                        row: node_row,
2900                    });
2901                }
2902            }
2903            let mut cursor = node.walk();
2904            let children: Vec<_> = node.named_children(&mut cursor).collect();
2905            for child in children.into_iter().rev() {
2906                worklist.push(child);
2907            }
2908            continue;
2909        }
2910
2911        // New expression nodes (e.g. "new_expression", "object_creation_expression")
2912        if config.new_expr_nodes.contains(&kind) {
2913            if let Some(type_node) = node.child_by_field_name(config.new_expr_type_field) {
2914                let name = type_node.utf8_text(source).unwrap_or("");
2915                let name = name.rsplit('.').next().unwrap_or(name);
2916                if !name.is_empty() && !is_builtin(name, config) {
2917                    refs.push(AstRef {
2918                        kind: AstRefKind::Call(name.to_string()),
2919                        row: node_row,
2920                    });
2921                }
2922            }
2923            let mut cursor = node.walk();
2924            let children: Vec<_> = node.named_children(&mut cursor).collect();
2925            for child in children.into_iter().rev() {
2926                worklist.push(child);
2927            }
2928            continue;
2929        }
2930
2931        // Composite literal nodes (e.g. Go "composite_literal")
2932        if config.composite_literal_nodes.contains(&kind) {
2933            if let Some(type_node) = node.child_by_field_name("type") {
2934                let name = type_node.utf8_text(source).unwrap_or("");
2935                if name.chars().next().map_or(false, |c| c.is_uppercase())
2936                    && !is_builtin(name, config)
2937                {
2938                    refs.push(AstRef {
2939                        kind: AstRefKind::Call(name.to_string()),
2940                        row: node_row,
2941                    });
2942                }
2943            }
2944        }
2945
2946        // Recurse into children
2947        let mut cursor = node.walk();
2948        let children: Vec<_> = node.named_children(&mut cursor).collect();
2949        for child in children.into_iter().rev() {
2950            worklist.push(child);
2951        }
2952    }
2953    refs
2954}
2955
2956/// Extract a call reference from a function/callee node (shared across languages)
2957fn extract_call_ref(
2958    func: tree_sitter::Node,
2959    _entity_id: &str,
2960    entity_name: &str,
2961    source: &[u8],
2962    refs: &mut Vec<AstRef>,
2963    config: &ScopeResolveConfig,
2964    row: usize,
2965) {
2966    let func_kind = func.kind();
2967
2968    if func_kind == "identifier" || func_kind == "simple_identifier" || func_kind == "type_identifier" {
2969        let name = func.utf8_text(source).unwrap_or("");
2970        if !name.is_empty() && name != entity_name && !is_builtin(name, config) {
2971            refs.push(AstRef {
2972                kind: AstRefKind::Call(name.to_string()),
2973                row,
2974            });
2975        }
2976        return;
2977    }
2978
2979    // Check config member_access patterns
2980    for ma in config.member_access {
2981        if func_kind == ma.node_kind {
2982            extract_member_call_ref(func, ma.object_field, ma.property_field, source, refs, row);
2983            return;
2984        }
2985    }
2986
2987    // Scoped call nodes (e.g. Rust "scoped_identifier" for Type::method)
2988    if config.scoped_call_nodes.contains(&func_kind) {
2989        let text = func.utf8_text(source).unwrap_or("");
2990        let parts: Vec<&str> = text.split("::").collect();
2991        if parts.len() >= 2 {
2992            // Handle super:: and self:: prefixed paths by emitting a call
2993            // to the final name so scope chain resolution can find it.
2994            let is_path_prefix =
2995                parts[0] == "super" || parts[0] == "self" || parts[0] == "crate";
2996            let method_name = parts[parts.len() - 1];
2997            if is_path_prefix {
2998                if !method_name.is_empty() && !is_builtin(method_name, config) {
2999                    refs.push(AstRef {
3000                        kind: AstRefKind::Call(method_name.to_string()),
3001                        row,
3002                    });
3003                }
3004            } else {
3005                let type_name = parts[parts.len() - 2];
3006                if !type_name.is_empty() && !method_name.is_empty() {
3007                    refs.push(AstRef {
3008                        kind: AstRefKind::Call(method_name.to_string()),
3009                        row,
3010                    });
3011                    if type_name.chars().next().map_or(false, |c| c.is_uppercase())
3012                        && !is_builtin(type_name, config)
3013                    {
3014                        refs.push(AstRef {
3015                            kind: AstRefKind::Call(type_name.to_string()),
3016                            row,
3017                        });
3018                    }
3019                }
3020            }
3021        }
3022    }
3023}
3024
3025/// Extract a member/method call from a node with object+property fields.
3026/// Falls back to positional children for languages like Kotlin where
3027/// navigation_expression children don't have field names.
3028fn extract_member_call_ref(
3029    node: tree_sitter::Node,
3030    object_field: &str,
3031    attr_field: &str,
3032    source: &[u8],
3033    refs: &mut Vec<AstRef>,
3034    row: usize,
3035) {
3036    let obj_text = node
3037        .child_by_field_name(object_field)
3038        .and_then(|n| n.utf8_text(source).ok())
3039        .unwrap_or("");
3040
3041    let attr_text = node
3042        .child_by_field_name(attr_field)
3043        .and_then(|n| {
3044            let text = n.utf8_text(source).ok()?;
3045            // Swift navigation_suffix includes the dot prefix (.validate → validate)
3046            Some(text.trim_start_matches('.'))
3047        })
3048        .unwrap_or("");
3049
3050    if !obj_text.is_empty() && !attr_text.is_empty() {
3051        push_method_call_ref(obj_text, attr_text, refs, row);
3052        return;
3053    }
3054
3055    // Fallback: positional children (Kotlin navigation_expression has no field names)
3056    let child_count = node.named_child_count();
3057    if child_count >= 2 {
3058        let obj = node.named_child(0)
3059            .and_then(|n| n.utf8_text(source).ok())
3060            .unwrap_or("");
3061        let last_idx = (child_count - 1) as u32;
3062        let attr = node.named_child(last_idx)
3063            .and_then(|n| n.utf8_text(source).ok())
3064            .unwrap_or("");
3065        if !obj.is_empty() && !attr.is_empty() {
3066            push_method_call_ref(obj, attr, refs, row);
3067        }
3068    }
3069}
3070
3071fn push_method_call_ref(obj: &str, method: &str, refs: &mut Vec<AstRef>, row: usize) {
3072    refs.push(AstRef {
3073        kind: AstRefKind::MethodCall {
3074            receiver: obj.to_string(),
3075            method: method.to_string(),
3076        },
3077        row,
3078    });
3079}
3080
3081/// Resolve a single reference against scopes and symbol tables.
3082fn resolve_ref(
3083    ast_ref: &AstRef,
3084    scope_idx: usize,
3085    scopes: &[Scope],
3086    symbol_table: &HashMap<String, Vec<String>>,
3087    class_members: &HashMap<String, Vec<(String, String)>>,
3088    import_table: &HashMap<(String, String), String>,
3089    instance_attr_types: &HashMap<(String, String), String>,
3090    entity_map: &HashMap<String, EntityInfo>,
3091    file_path: &str,
3092    from_entity_id: &str,
3093    allow_cross_file_calls: bool,
3094) -> Option<(String, RefType, &'static str)> {
3095    match &ast_ref.kind {
3096        AstRefKind::Call(name) => {
3097            if is_local_binding_in_scopes(scope_idx, scopes, name) {
3098                return None;
3099            }
3100
3101            // 1. Walk scope chain for the name
3102            if let Some(eid) = lookup_scope_chain(scope_idx, scopes, name) {
3103                if eid != from_entity_id {
3104                    return Some((eid, RefType::Calls, "scope_chain"));
3105                }
3106            }
3107
3108            // 2. Check import table
3109            let key = (file_path.to_string(), name.clone());
3110            if let Some(target_id) = import_table.get(&key) {
3111                return Some((target_id.clone(), RefType::Calls, "import"));
3112            }
3113
3114            // 3. Global symbol table fallback (constructor calls or cross-file functions)
3115            if let Some(target_ids) = symbol_table.get(name.as_str()) {
3116                let is_constructor = name.chars().next().map_or(false, |c| c.is_uppercase());
3117                let ref_type = if is_constructor { RefType::TypeRef } else { RefType::Calls };
3118                // Prefer same-file, then any
3119                let target = target_ids
3120                    .iter()
3121                    .find(|id| {
3122                        entity_map
3123                            .get(*id)
3124                            .map_or(false, |e| e.file_path == file_path)
3125                    })
3126                    .or_else(|| {
3127                        // Fall back to cross-file global for constructor calls.
3128                        // For lowercase calls, require explicit imports to avoid false positives,
3129                        // unless the language allows implicit cross-file visibility (Swift, Kotlin).
3130                        if is_constructor || allow_cross_file_calls {
3131                            target_ids.first()
3132                        } else {
3133                            None
3134                        }
3135                    });
3136                if let Some(tid) = target {
3137                    return Some((tid.clone(), ref_type, "scope_chain"));
3138                }
3139            }
3140
3141            None
3142        }
3143
3144        AstRefKind::MethodCall { receiver: raw_receiver, method } => {
3145            // Strip prefix operators like ! (Swift: `!dog.validate()`)
3146            let receiver = raw_receiver.trim_start_matches('!').trim_start_matches('~');
3147            if receiver == "self" || receiver == "this" {
3148                // self.method() -> find in enclosing class
3149                let mut idx = scope_idx;
3150                loop {
3151                    if scopes[idx].kind == "class" {
3152                        if let Some(eid) = scopes[idx].defs.get(method.as_str()) {
3153                            return Some((eid.clone(), RefType::Calls, "scope_chain"));
3154                        }
3155                        break;
3156                    }
3157                    match scopes[idx].parent {
3158                        Some(p) => idx = p,
3159                        None => break,
3160                    }
3161                }
3162                return None;
3163            }
3164
3165            // Handle chained self.attr.method() pattern
3166            // receiver is "self.X" where X is an instance attribute
3167            if receiver.starts_with("self.") || receiver.starts_with("this.") {
3168                let attr_name = &receiver[5..]; // strip "self." or "this."
3169                // Find the enclosing class name
3170                let class_name = find_enclosing_class(scope_idx, scopes, entity_map);
3171                if let Some(cn) = class_name {
3172                    // Look up instance attribute type
3173                    if let Some(attr_type) = instance_attr_types.get(&(cn, attr_name.to_string())) {
3174                        if let Some(members) = class_members.get(attr_type.as_str()) {
3175                            if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
3176                                return Some((mid.clone(), RefType::Calls, "type_tracking"));
3177                            }
3178                        }
3179                    }
3180                }
3181            }
3182
3183            // Handle chained var.field.method() pattern (e.g. Go receiver: t.Conn.Execute())
3184            if receiver.contains('.') && !receiver.starts_with("self.") && !receiver.starts_with("this.") {
3185                if let Some(dot_pos) = receiver.find('.') {
3186                    let var_part = &receiver[..dot_pos];
3187                    let field_part = &receiver[dot_pos + 1..];
3188                    if let Some(var_type) = lookup_type_in_scopes(scope_idx, scopes, var_part) {
3189                        if let Some(attr_type) = instance_attr_types.get(&(var_type, field_part.to_string())) {
3190                            if let Some(members) = class_members.get(attr_type.as_str()) {
3191                                if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
3192                                    return Some((mid.clone(), RefType::Calls, "type_tracking"));
3193                                }
3194                            }
3195                        }
3196                    }
3197                }
3198            }
3199
3200            // receiver.method() -> look up receiver type, then resolve method
3201            let receiver_type = lookup_type_in_scopes(scope_idx, scopes, receiver);
3202
3203            if let Some(class_name) = receiver_type {
3204                if let Some(members) = class_members.get(class_name.as_str()) {
3205                    if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
3206                        return Some((mid.clone(), RefType::Calls, "type_tracking"));
3207                    }
3208                }
3209            }
3210
3211            // ClassName.method() static call, only when ClassName is visible and
3212            // not shadowed by a local binding.
3213            if !is_local_binding_in_scopes(scope_idx, scopes, receiver) {
3214                if let Some(class_id) = lookup_scope_chain(scope_idx, scopes, receiver) {
3215                    if let Some(info) = entity_map.get(&class_id) {
3216                        if matches!(info.entity_type.as_str(), "class" | "struct" | "interface")
3217                            && info.name == receiver
3218                        {
3219                            if let Some(members) = class_members.get(&info.name) {
3220                                if let Some((_, mid)) = members.iter().find(|(n, _)| n == method) {
3221                                    return Some((mid.clone(), RefType::Calls, "scope_chain"));
3222                                }
3223                            }
3224                        }
3225                    }
3226                }
3227            }
3228
3229            // Fallback: check import table for the receiver
3230            if !is_local_binding_in_scopes(scope_idx, scopes, receiver) {
3231                let key = (file_path.to_string(), receiver.to_string());
3232                if let Some(target_id) = import_table.get(&key) {
3233                    if let Some(info) = entity_map.get(target_id) {
3234                        if matches!(info.entity_type.as_str(), "class" | "struct") {
3235                            if let Some(members) = class_members.get(&info.name) {
3236                                if let Some((_, mid)) =
3237                                    members.iter().find(|(n, _)| n == method)
3238                                {
3239                                    return Some((
3240                                        mid.clone(),
3241                                        RefType::Calls,
3242                                        "type_tracking",
3243                                    ));
3244                                }
3245                            }
3246                        }
3247                    }
3248                }
3249
3250                // Namespace import: alias.method()
3251                let key = (file_path.to_string(), format!("{receiver}.{method}"));
3252                if let Some(target_id) = import_table.get(&key) {
3253                    return Some((target_id.clone(), RefType::Calls, "import"));
3254                }
3255            }
3256
3257            // Go package-qualified call: package.Function()
3258            // Try the method name directly in the import table
3259            if file_path.ends_with(".go") {
3260                let key = (file_path.to_string(), method.clone());
3261                if let Some(target_id) = import_table.get(&key) {
3262                    return Some((target_id.clone(), RefType::Calls, "import"));
3263                }
3264            }
3265
3266            None
3267        }
3268    }
3269}
3270
3271/// Find the class name for the enclosing class scope.
3272fn find_enclosing_class(
3273    start_scope: usize,
3274    scopes: &[Scope],
3275    entity_map: &HashMap<String, EntityInfo>,
3276) -> Option<String> {
3277    let mut idx = start_scope;
3278    loop {
3279        if scopes[idx].kind == "class" {
3280            if let Some(ref oid) = scopes[idx].owner_id {
3281                return entity_map.get(oid).map(|e| e.name.clone());
3282            }
3283        }
3284        match scopes[idx].parent {
3285            Some(p) => idx = p,
3286            None => return None,
3287        }
3288    }
3289}
3290
3291/// Walk up the scope chain looking for a definition.
3292fn lookup_scope_chain(
3293    start_scope: usize,
3294    scopes: &[Scope],
3295    name: &str,
3296) -> Option<String> {
3297    let mut idx = start_scope;
3298    loop {
3299        if let Some(eid) = scopes[idx].defs.get(name) {
3300            return Some(eid.clone());
3301        }
3302        match scopes[idx].parent {
3303            Some(p) => idx = p,
3304            None => return None,
3305        }
3306    }
3307}
3308
3309/// Walk up the scope chain looking for a local binding that shadows a definition.
3310fn is_local_binding_in_scopes(
3311    start_scope: usize,
3312    scopes: &[Scope],
3313    name: &str,
3314) -> bool {
3315    let mut idx = start_scope;
3316    loop {
3317        if scopes[idx].bindings.contains(name) {
3318            return true;
3319        }
3320        match scopes[idx].parent {
3321            Some(p) => idx = p,
3322            None => return false,
3323        }
3324    }
3325}
3326
3327/// Walk up the scope chain looking for a type binding.
3328fn lookup_type_in_scopes(
3329    start_scope: usize,
3330    scopes: &[Scope],
3331    var_name: &str,
3332) -> Option<String> {
3333    let mut idx = start_scope;
3334    loop {
3335        if let Some(type_name) = scopes[idx].types.get(var_name) {
3336            return Some(type_name.clone());
3337        }
3338        match scopes[idx].parent {
3339            Some(p) => idx = p,
3340            None => return None,
3341        }
3342    }
3343}
3344
3345fn is_builtin(name: &str, config: &ScopeResolveConfig) -> bool {
3346    // Common builtins across languages
3347    if matches!(name, "None" | "True" | "False" | "null" | "undefined" | "nil") {
3348        return true;
3349    }
3350    config.builtins.contains(&name)
3351}