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::cmp::Ordering;
15use std::collections::{HashMap, HashSet};
16use std::path::Path;
17use std::sync::{Arc, Mutex, OnceLock};
18
19#[cfg(feature = "parallel")]
20use rayon::prelude::*;
21
22use crate::model::entity::SemanticEntity;
23
24macro_rules! maybe_par_iter {
25    ($slice:expr) => {{
26        #[cfg(feature = "parallel")]
27        {
28            $slice.par_iter()
29        }
30        #[cfg(not(feature = "parallel"))]
31        {
32            $slice.iter()
33        }
34    }};
35}
36use crate::parser::graph::{EntityInfo, RefType};
37use crate::parser::import_resolution::{
38    find_import_file, find_import_target, import_source_matches_file, is_js_ts_file,
39    js_ts_named_exports_from_content, sort_import_candidate_files, JS_TS_EXTENSIONS,
40};
41use crate::parser::plugins::code::languages::{
42    get_language_config, AssignmentStrategy, CallNodeStyle, ClassNameField, InitStrategy,
43    ParamNameField, ScopeResolveConfig,
44};
45
46type AttrToParamIndex<'a> = HashMap<(&'a str, &'a str), Vec<(&'a str, &'a str)>>;
47
48/// A scope in the scope tree. Scopes are nested: module -> class -> function -> block.
49pub struct Scope {
50    parent: Option<usize>,
51    /// Definitions visible in this scope: name -> entity_id
52    defs: HashMap<String, String>,
53    /// Local bindings that shadow outer names but are not graph entities.
54    bindings: HashSet<String>,
55    /// Binding declaration rows keyed by name.
56    binding_rows: HashMap<String, Vec<usize>>,
57    /// Variable type bindings: var_name -> class_name (from `x = Foo()`)
58    types: HashMap<String, String>,
59    /// Unresolved call assignments: var_name -> function_name (from `x = func()`)
60    /// These get resolved after return type analysis.
61    pending_call_types: HashMap<String, String>,
62    /// Which entity owns this scope (if any)
63    owner_id: Option<String>,
64    /// What kind of scope: "module", "class", "function"
65    kind: &'static str,
66}
67
68/// Reference found in the AST
69struct AstRef {
70    /// Kind of reference
71    kind: AstRefKind,
72    /// Row (0-indexed) where this reference appears in the source
73    row: usize,
74    /// Byte range for the referenced syntax node in the file.
75    start_byte: usize,
76    end_byte: usize,
77}
78
79enum AstRefKind {
80    /// Bare name call: `foo()`
81    Call {
82        name: String,
83        argument_labels: Option<Vec<Option<String>>>,
84    },
85    /// Qualified path call: `module::function()`
86    ScopedCall { path: String, name: String },
87    /// Attribute call: `x.method()`
88    MethodCall {
89        receiver: String,
90        method: String,
91        argument_labels: Option<Vec<Option<String>>>,
92    },
93}
94
95struct SwiftCallSignature {
96    argument_labels: Vec<Option<String>>,
97}
98
99enum SwiftOverloadSelection {
100    Matched(String),
101    NoMatch,
102    NotApplicable,
103}
104
105#[derive(Clone, Copy)]
106struct SourceSpan {
107    start_byte: usize,
108    end_byte: usize,
109}
110
111fn entity_creates_reference_scope(entity_type: &str) -> bool {
112    matches!(
113        entity_type,
114        "function"
115            | "method"
116            | "constructor"
117            | "init"
118            | "init_declaration"
119            | "class"
120            | "struct"
121            | "interface"
122            | "impl"
123            | "enum"
124            | "protocol"
125            | "protocol_declaration"
126            | "object_declaration"
127            | "companion_object"
128            | "extension"
129            | "module"
130            | "namespace"
131    )
132}
133
134/// A reference-scope child as needed to decide ref ownership: its line range and
135/// (if known) byte span. Precomputed once per entity so the per-ref ownership test
136/// does no HashMap lookups.
137type ChildRefCheck = (usize, usize, Option<(usize, usize)>);
138
139/// Whether `ast_ref` belongs directly to an entity (inside its span, not inside any
140/// of its reference-scope children). `entity_span` and `child_ref_checks` are fetched
141/// once per entity by the caller; this keeps the hot per-ref loop allocation- and
142/// hash-free.
143fn ref_owned_by_entity(
144    ast_ref: &AstRef,
145    entity_span: Option<SourceSpan>,
146    child_ref_checks: &[ChildRefCheck],
147) -> bool {
148    if let Some(entity_span) = entity_span {
149        if ast_ref.end_byte <= entity_span.start_byte || ast_ref.start_byte >= entity_span.end_byte
150        {
151            return false;
152        }
153    }
154
155    let source_line = ast_ref.row + 1;
156    child_ref_checks
157        .iter()
158        .all(|(child_start_line, child_end_line, child_span)| {
159            if source_line < *child_start_line || source_line > *child_end_line {
160                return true;
161            }
162            match child_span {
163                Some((start_byte, end_byte)) => {
164                    ast_ref.end_byte <= *start_byte || ast_ref.start_byte >= *end_byte
165                }
166                None => false,
167            }
168        })
169}
170
171fn find_entity_source_spans<'a>(
172    entities: &[&'a SemanticEntity],
173    source: &str,
174) -> HashMap<&'a str, SourceSpan> {
175    let mut spans = HashMap::new();
176    let line_starts = source_line_starts(source);
177    for entity in entities {
178        if entity.content.is_empty() {
179            continue;
180        }
181
182        if let Some(span) = find_entity_source_span(entity, source, &line_starts) {
183            spans.insert(entity.id.as_str(), span);
184        }
185    }
186    spans
187}
188
189fn source_line_starts(source: &str) -> Vec<usize> {
190    let mut starts = vec![0];
191    for (idx, byte) in source.bytes().enumerate() {
192        if byte == b'\n' && idx + 1 < source.len() {
193            starts.push(idx + 1);
194        }
195    }
196    starts
197}
198
199fn find_entity_source_span(
200    entity: &SemanticEntity,
201    source: &str,
202    line_starts: &[usize],
203) -> Option<SourceSpan> {
204    if entity.file_path.ends_with(".swift") && entity.entity_type == "property" {
205        if let Some(span) = swift_property_binding_span(entity, source.as_bytes(), line_starts) {
206            return Some(span);
207        }
208    }
209
210    let line_index = entity.start_line.checked_sub(1)?;
211    let line_start = *line_starts.get(line_index)?;
212
213    if let Some(span) = source_span_at(source, &entity.content, line_start) {
214        return Some(span);
215    }
216
217    let line_end = line_starts
218        .get(line_index + 1)
219        .copied()
220        .unwrap_or(source.len());
221    let line = source.get(line_start..line_end)?;
222    let trimmed_line_start = line_start + line.len().saturating_sub(line.trim_start().len());
223    if trimmed_line_start != line_start {
224        if let Some(span) = source_span_at(source, &entity.content, trimmed_line_start) {
225            return Some(span);
226        }
227    }
228
229    let first_content_line = entity.content.lines().next().unwrap_or("").trim_start();
230    if first_content_line.is_empty() {
231        return None;
232    }
233
234    for (candidate_offset, _) in line.match_indices(first_content_line) {
235        if let Some(span) = source_span_at(source, &entity.content, line_start + candidate_offset) {
236            return Some(span);
237        }
238    }
239
240    None
241}
242
243fn source_span_at(source: &str, content: &str, start_byte: usize) -> Option<SourceSpan> {
244    if source.get(start_byte..)?.starts_with(content) {
245        Some(SourceSpan {
246            start_byte,
247            end_byte: start_byte + content.len(),
248        })
249    } else {
250        None
251    }
252}
253
254fn line_start(line_starts: &[usize], line: usize) -> usize {
255    line_starts
256        .get(line.saturating_sub(1))
257        .copied()
258        .unwrap_or(0)
259}
260
261fn line_end(line_starts: &[usize], source_len: usize, line: usize) -> usize {
262    line_starts
263        .get(line)
264        .copied()
265        .map(|offset| offset.saturating_sub(1))
266        .unwrap_or(source_len)
267}
268
269fn swift_property_binding_span(
270    entity: &SemanticEntity,
271    source: &[u8],
272    line_starts: &[usize],
273) -> Option<SourceSpan> {
274    let search_start = line_start(line_starts, entity.start_line);
275    let search_end = line_end(line_starts, source.len(), entity.end_line).min(source.len());
276    let haystack = source.get(search_start..search_end)?;
277    let content = entity.content.trim();
278    if !content.is_empty() {
279        if let Some(local_start) = find_subslice(haystack, content.as_bytes()) {
280            let start = search_start + local_start;
281            return Some(SourceSpan {
282                start_byte: start,
283                end_byte: start + content.len(),
284            });
285        }
286    }
287
288    let name = entity.name.as_bytes();
289    if name.is_empty() {
290        return None;
291    }
292    let mut local_search_start = 0;
293    while let Some(local_start) = find_subslice(&haystack[local_search_start..], name) {
294        let local_start = local_search_start + local_start;
295        let start = search_start + local_start;
296        let end = start + entity.name.len();
297        if !identifier_boundary(source, start, end) {
298            local_search_start = local_start + name.len();
299            continue;
300        }
301        let segment_end = swift_binding_segment_end(source, end, search_end);
302        return Some(SourceSpan {
303            start_byte: start,
304            end_byte: segment_end,
305        });
306    }
307    None
308}
309
310fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
311    if needle.is_empty() {
312        return Some(0);
313    }
314    haystack
315        .windows(needle.len())
316        .position(|window| window == needle)
317}
318
319fn swift_binding_segment_end(source: &[u8], start: usize, search_end: usize) -> usize {
320    let mut depth = 0usize;
321    let mut idx = start;
322    let mut string_delimiter: Option<u8> = None;
323    while idx < search_end {
324        let byte = source[idx];
325        if let Some(delimiter) = string_delimiter {
326            if byte == b'\\' {
327                idx = (idx + 2).min(search_end);
328                continue;
329            }
330            if byte == delimiter {
331                string_delimiter = None;
332            }
333            idx += 1;
334            continue;
335        }
336
337        match byte {
338            b'"' | b'\'' => string_delimiter = Some(byte),
339            b'(' | b'[' | b'{' => depth += 1,
340            b')' | b']' | b'}' => depth = depth.saturating_sub(1),
341            b',' if depth == 0 => return idx,
342            _ => {}
343        }
344        idx += 1;
345    }
346    search_end
347}
348
349fn identifier_boundary(source: &[u8], start: usize, end: usize) -> bool {
350    let before = start
351        .checked_sub(1)
352        .and_then(|idx| source.get(idx))
353        .copied();
354    let after = source.get(end).copied();
355    !before.map_or(false, is_identifier_byte) && !after.map_or(false, is_identifier_byte)
356}
357
358fn is_identifier_byte(byte: u8) -> bool {
359    byte.is_ascii_alphanumeric() || byte == b'_'
360}
361
362/// Result of scope-aware resolution
363pub struct ScopeResult {
364    pub edges: Vec<(String, String, RefType)>,
365    /// Debug info: which references were resolved and how
366    pub resolution_log: Vec<ResolutionEntry>,
367}
368
369pub(crate) struct ScopeResultFull {
370    pub(crate) edges: Vec<(String, String, RefType)>,
371    pub(crate) resolution_log: Vec<ResolutionEntry>,
372    pub(crate) consumed_words: HashMap<String, HashSet<String>>,
373}
374
375#[derive(Clone)]
376pub struct ResolutionEntry {
377    pub from_entity: String,
378    pub reference: String,
379    pub resolved_to: Option<String>,
380    pub method: &'static str, // "scope_chain", "type_tracking", "import", "unresolved", "local_binding"
381}
382
383/// Resolve references using tree-sitter scope analysis.
384///
385/// For each file:
386/// 1. Parse with tree-sitter
387/// 2. Build a scope tree (module -> class -> function)
388/// 3. Walk entity AST subtrees to find reference nodes
389/// 4. Resolve each reference via scope chain + type tracking
390/// Pre-built lookup tables that can be shared between `EntityGraph::build()` and
391/// `resolve_with_scopes()` to avoid redundant O(E) passes.
392pub(crate) struct PreBuiltLookups {
393    pub(crate) symbol_table: Arc<HashMap<String, Vec<String>>>,
394    pub(crate) class_members: HashMap<String, Vec<(String, String)>>,
395    pub(crate) owner_members: HashMap<String, Vec<(String, String)>>,
396    pub(crate) entity_ranges: HashMap<String, Vec<(usize, usize, String)>>,
397    /// Go package index: pkg_name → [(entity_name, entity_id)]
398    /// Avoids O(symbol_table) scan per Go import.
399    pub(crate) go_pkg_index: HashMap<String, Vec<(String, String)>>,
400}
401
402struct TsDefaultExportTable {
403    exports_by_file: HashMap<String, String>,
404    sorted_files: Vec<String>,
405}
406
407struct TsDefaultReExport {
408    file_path: String,
409    original_name: String,
410    module_path: String,
411}
412
413struct TopLevelEntityIndex {
414    entities_by_file: HashMap<String, Vec<(String, String)>>,
415    sorted_files: Vec<String>,
416}
417
418struct FileEntityLookup<'a> {
419    by_name: HashMap<&'a str, Vec<&'a SemanticEntity>>,
420}
421
422impl<'a> FileEntityLookup<'a> {
423    fn new(file_entities: &[&'a SemanticEntity]) -> Self {
424        let mut by_name: HashMap<&'a str, Vec<&'a SemanticEntity>> = HashMap::new();
425        for entity in file_entities {
426            by_name
427                .entry(entity.name.as_str())
428                .or_default()
429                .push(*entity);
430        }
431        Self { by_name }
432    }
433
434    /// First entity ID for `name` defined in this file, in entity-discovery order.
435    /// Equivalent to scanning the global symbol table for same-file candidates and
436    /// taking the first, but O(1) instead of O(entities-sharing-this-name).
437    fn first_id_by_name(&self, name: &str) -> Option<&'a str> {
438        self.by_name
439            .get(name)
440            .and_then(|entities| entities.first())
441            .map(|entity| entity.id.as_str())
442    }
443
444    fn find_at_line<F>(
445        &self,
446        name: &str,
447        line: usize,
448        type_matches: F,
449    ) -> Option<&'a SemanticEntity>
450    where
451        F: Fn(&SemanticEntity) -> bool,
452    {
453        if name.is_empty() {
454            return None;
455        }
456        self.by_name.get(name)?.iter().find_map(|entity| {
457            if entity.start_line <= line && line <= entity.end_line && type_matches(entity) {
458                Some(*entity)
459            } else {
460                None
461            }
462        })
463    }
464}
465
466#[derive(Default)]
467struct ScopeLookupCache {
468    defs: HashMap<usize, HashMap<String, Option<String>>>,
469    local_bindings: HashMap<usize, HashMap<String, bool>>,
470    types: HashMap<usize, HashMap<String, Option<String>>>,
471    enclosing_classes: HashMap<usize, Option<String>>,
472}
473
474pub(crate) fn class_member_owner_name(parent: &EntityInfo) -> Option<&str> {
475    matches!(
476        parent.entity_type.as_str(),
477        "class"
478            | "struct"
479            | "interface"
480            | "impl"
481            | "enum"
482            | "protocol"
483            | "protocol_declaration"
484            | "object_declaration"
485            | "companion_object"
486            | "extension"
487    )
488    .then_some(parent.name.as_str())
489}
490
491fn sort_symbol_table_targets_by_source(
492    symbol_table: &mut HashMap<String, Vec<String>>,
493    entity_map: &HashMap<String, EntityInfo>,
494) {
495    for target_ids in symbol_table.values_mut() {
496        if target_ids.len() > 1 {
497            target_ids.sort_unstable_by(|left, right| {
498                compare_entity_ids_by_source(left, right, entity_map)
499            });
500        }
501    }
502}
503
504fn compare_entity_ids_by_source(
505    left: &str,
506    right: &str,
507    entity_map: &HashMap<String, EntityInfo>,
508) -> Ordering {
509    match (entity_map.get(left), entity_map.get(right)) {
510        (Some(left), Some(right)) => (
511            left.file_path.as_str(),
512            left.start_line,
513            left.end_line,
514            left.id.as_str(),
515        )
516            .cmp(&(
517                right.file_path.as_str(),
518                right.start_line,
519                right.end_line,
520                right.id.as_str(),
521            )),
522        (Some(_), None) => Ordering::Less,
523        (None, Some(_)) => Ordering::Greater,
524        (None, None) => left.cmp(right),
525    }
526}
527
528/// Public API — preserves the original 5-parameter signature for semver compatibility.
529pub fn resolve_with_scopes(
530    root: &Path,
531    file_paths: &[String],
532    all_entities: &[SemanticEntity],
533    entity_map: &HashMap<String, EntityInfo>,
534    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
535) -> ScopeResult {
536    let result = resolve_with_scopes_full(
537        root,
538        file_paths,
539        all_entities,
540        entity_map,
541        pre_parsed,
542        None,
543        None,
544        true,
545    );
546    ScopeResult {
547        edges: result.edges,
548        resolution_log: result.resolution_log,
549    }
550}
551
552/// Internal version with pre-built lookups for performance.
553pub(crate) fn resolve_with_scopes_full(
554    root: &Path,
555    file_paths: &[String],
556    all_entities: &[SemanticEntity],
557    entity_map: &HashMap<String, EntityInfo>,
558    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
559    pre_built: Option<&PreBuiltLookups>,
560    pre_built_import_table: Option<&HashMap<(String, String), String>>,
561    emit_local_binding_log: bool,
562) -> ScopeResultFull {
563    resolve_with_scopes_full_inner(
564        root,
565        file_paths,
566        all_entities,
567        entity_map,
568        pre_parsed,
569        pre_built,
570        pre_built_import_table,
571        emit_local_binding_log,
572        None,
573    )
574}
575
576pub(crate) fn resolve_with_scopes_full_for_entities(
577    root: &Path,
578    file_paths: &[String],
579    all_entities: &[SemanticEntity],
580    entity_map: &HashMap<String, EntityInfo>,
581    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
582    pre_built: Option<&PreBuiltLookups>,
583    pre_built_import_table: Option<&HashMap<(String, String), String>>,
584    emit_entity_ids: &HashSet<&str>,
585) -> ScopeResultFull {
586    resolve_with_scopes_full_inner(
587        root,
588        file_paths,
589        all_entities,
590        entity_map,
591        pre_parsed,
592        pre_built,
593        pre_built_import_table,
594        false,
595        Some(emit_entity_ids),
596    )
597}
598
599fn resolve_with_scopes_full_inner(
600    root: &Path,
601    file_paths: &[String],
602    all_entities: &[SemanticEntity],
603    entity_map: &HashMap<String, EntityInfo>,
604    pre_parsed: Option<Vec<(String, String, tree_sitter::Tree)>>,
605    pre_built: Option<&PreBuiltLookups>,
606    pre_built_import_table: Option<&HashMap<(String, String), String>>,
607    emit_local_binding_log: bool,
608    emit_entity_ids: Option<&HashSet<&str>>,
609) -> ScopeResultFull {
610    let mut all_edges: Vec<(String, String, RefType)> = Vec::new();
611    let mut log: Vec<ResolutionEntry> = Vec::new();
612    let mut consumed_words: HashMap<String, HashSet<String>> = HashMap::new();
613
614    // Use pre-built lookups if provided, otherwise build from scratch.
615    let owned_lookups;
616    let lookups = if let Some(pb) = pre_built {
617        pb
618    } else {
619        let mut symbol_table: HashMap<String, Vec<String>> = HashMap::new();
620        let mut class_members: HashMap<String, Vec<(String, String)>> = HashMap::new();
621        let mut owner_members: HashMap<String, Vec<(String, String)>> = HashMap::new();
622        let mut entity_ranges: HashMap<String, Vec<(usize, usize, String)>> = HashMap::new();
623
624        for entity in all_entities {
625            symbol_table
626                .entry(entity.name.clone())
627                .or_default()
628                .push(entity.id.clone());
629
630            if let Some(ref pid) = entity.parent_id {
631                owner_members
632                    .entry(pid.clone())
633                    .or_default()
634                    .push((entity.name.clone(), entity.id.clone()));
635                if let Some(parent) = entity_map.get(pid) {
636                    if let Some(owner_name) = class_member_owner_name(parent) {
637                        class_members
638                            .entry(owner_name.to_string())
639                            .or_default()
640                            .push((entity.name.clone(), entity.id.clone()));
641                    }
642                }
643            }
644
645            if entity.entity_type == "method" && entity.file_path.ends_with(".go") {
646                if let Some(struct_name) = extract_go_receiver_type(&entity.content) {
647                    class_members
648                        .entry(struct_name)
649                        .or_default()
650                        .push((entity.name.clone(), entity.id.clone()));
651                }
652            }
653
654            entity_ranges
655                .entry(entity.file_path.clone())
656                .or_default()
657                .push((entity.start_line, entity.end_line, entity.id.clone()));
658        }
659        sort_symbol_table_targets_by_source(&mut symbol_table, entity_map);
660        for members in class_members.values_mut() {
661            members.sort_unstable();
662        }
663        for members in owner_members.values_mut() {
664            members.sort_unstable();
665        }
666        for ranges in entity_ranges.values_mut() {
667            ranges.sort_unstable();
668        }
669
670        // Build Go package index for O(1) import lookup
671        let go_pkg_index = build_go_pkg_index(&symbol_table, entity_map);
672
673        owned_lookups = PreBuiltLookups {
674            symbol_table: Arc::new(symbol_table),
675            class_members,
676            owner_members,
677            entity_ranges,
678            go_pkg_index,
679        };
680        &owned_lookups
681    };
682    let symbol_table = lookups.symbol_table.as_ref();
683    let class_members = &lookups.class_members;
684    let owner_members = &lookups.owner_members;
685    let entity_ranges = &lookups.entity_ranges;
686    let go_pkg_index = &lookups.go_pkg_index;
687
688    // Build file-path indexed entity lookup: file_path -> Vec<&SemanticEntity>
689    let mut entities_by_file: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
690    for entity in all_entities {
691        entities_by_file
692            .entry(entity.file_path.as_str())
693            .or_default()
694            .push(entity);
695    }
696
697    // Build parent_id indexed entity lookup: parent_id -> Vec<&SemanticEntity>
698    let mut children_by_parent: HashMap<&str, Vec<&SemanticEntity>> = HashMap::new();
699    for entity in all_entities {
700        if let Some(ref pid) = entity.parent_id {
701            children_by_parent
702                .entry(pid.as_str())
703                .or_default()
704                .push(entity);
705        }
706    }
707
708    // Return type map: function_entity_id -> class_name (if function returns ClassName())
709    let mut return_type_map: HashMap<String, String> = HashMap::new();
710
711    // Instance attribute types: (class_name, attr_name) -> class_name_of_attr
712    let mut instance_attr_types: HashMap<(String, String), String> = HashMap::new();
713
714    // __init__ param info: class_name -> (ordered_params, attr_to_param mapping)
715    // attr_to_param: attr_name -> param_name (for self.attr = param patterns)
716    let mut init_params: HashMap<String, Vec<String>> = HashMap::new();
717    let mut attr_to_param: HashMap<(String, String), String> = HashMap::new();
718
719    // Merge pre-parsed trees with disk-parsed trees for missing files
720    let mut owned_parsed_files: Vec<(String, String, tree_sitter::Tree)> = Vec::new();
721    let pre_set: std::collections::HashSet<String> = if let Some(pp) = pre_parsed {
722        let set = pp.iter().map(|(fp, _, _)| fp.clone()).collect();
723        owned_parsed_files = pp;
724        set
725    } else {
726        std::collections::HashSet::new()
727    };
728    // Parse any files not already in the pre-parsed set
729    for file_path in file_paths {
730        if pre_set.contains(file_path) {
731            continue;
732        }
733        let full_path = root.join(file_path);
734        let content = match std::fs::read_to_string(&full_path) {
735            Ok(c) => c,
736            Err(_) => continue,
737        };
738        let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
739        let config = match get_language_config(ext) {
740            Some(c) => c,
741            None => continue,
742        };
743        let language = match (config.get_language)() {
744            Some(l) => l,
745            None => continue,
746        };
747        let mut parser = tree_sitter::Parser::new();
748        let _ = parser.set_language(&language);
749        if let Some(tree) = parser.parse(content.as_bytes(), None) {
750            owned_parsed_files.push((file_path.clone(), content, tree));
751        }
752    }
753    let parsed_files: &[(String, String, tree_sitter::Tree)] = &owned_parsed_files;
754    let content_by_file = OnceLock::new();
755    let exported_names_by_file: Mutex<HashMap<String, Arc<HashSet<String>>>> =
756        Mutex::new(HashMap::new());
757    // The default-export table is consulted only while resolving JS/TS imports.
758    // When an import table is supplied (the graph-build path), those imports are
759    // already resolved and `extract_ts_import`/`extract_ts_re_export` are skipped,
760    // so the table is never read — building it would be pure waste on a large repo.
761    let ts_default_exports = if pre_built_import_table.is_some() {
762        TsDefaultExportTable {
763            exports_by_file: HashMap::new(),
764            sorted_files: Vec::new(),
765        }
766    } else {
767        build_ts_default_export_table(parsed_files, &symbol_table, entity_map)
768    };
769    let top_level_entities = OnceLock::new();
770
771    // Pass 1: Scan ALL files for return types and instance attr types first
772    // This ensures cross-file return type info is available during resolution
773    // Parallelized: each file produces local maps, then merged sequentially.
774    let pass1_results: Vec<(
775        HashMap<String, String>,
776        HashMap<(String, String), String>,
777        HashMap<String, Vec<String>>,
778        HashMap<(String, String), String>,
779    )> = maybe_par_iter!(parsed_files)
780        .filter_map(|(file_path, content, tree)| {
781            let source = content.as_bytes();
782            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
783            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
784
785            let file_entities = entities_by_file
786                .get(file_path.as_str())
787                .map(|v| v.as_slice())
788                .unwrap_or(&[]);
789            let file_lookup = FileEntityLookup::new(file_entities);
790
791            let mut local_return_type_map: HashMap<String, String> = HashMap::new();
792            scan_return_types(
793                tree.root_node(),
794                file_path,
795                &file_lookup,
796                source,
797                &mut local_return_type_map,
798                config,
799            );
800
801            let mut local_instance_attr_types: HashMap<(String, String), String> = HashMap::new();
802            let mut local_init_params: HashMap<String, Vec<String>> = HashMap::new();
803            let mut local_attr_to_param: HashMap<(String, String), String> = HashMap::new();
804            scan_init_self_attrs(
805                tree.root_node(),
806                file_path,
807                file_entities,
808                entity_map,
809                source,
810                &mut local_instance_attr_types,
811                &mut local_init_params,
812                &mut local_attr_to_param,
813                config,
814            );
815
816            Some((
817                local_return_type_map,
818                local_instance_attr_types,
819                local_init_params,
820                local_attr_to_param,
821            ))
822        })
823        .collect();
824
825    for (local_rtm, local_iat, local_ip, local_atp) in pass1_results {
826        return_type_map.extend(local_rtm);
827        instance_attr_types.extend(local_iat);
828        init_params.extend(local_ip);
829        attr_to_param.extend(local_atp);
830    }
831
832    // Pass 1b: Infer constructor parameter types from call sites
833    // For `Transaction(get_connection())`, infer conn param has type Connection.
834    // Then resolve self.conn = conn -> (Transaction, conn) -> Connection
835    infer_constructor_param_types(
836        parsed_files,
837        &return_type_map,
838        &init_params,
839        &attr_to_param,
840        &symbol_table,
841        &mut instance_attr_types,
842    );
843    let func_name_return_types = deterministic_return_types_by_name(&return_type_map, symbol_table);
844
845    let swift_call_signatures = if parsed_files
846        .iter()
847        .any(|(file_path, _, _)| file_path.ends_with(".swift"))
848    {
849        build_swift_call_signatures(parsed_files, all_entities, &entity_ranges, entity_map)
850    } else {
851        HashMap::new()
852    };
853
854    // Group the prebuilt import table by importing file once. Otherwise every file
855    // in Pass 2 would rescan the entire table to find its own entries — O(files ×
856    // imports), which is quadratic on a large repo. Grouping makes each file O(its
857    // own imports).
858    let import_table_by_file: HashMap<&str, Vec<(&str, &str)>> =
859        if let Some(import_table) = pre_built_import_table {
860            let mut grouped: HashMap<&str, Vec<(&str, &str)>> = HashMap::new();
861            for ((import_file_path, local_name), target_id) in import_table {
862                grouped
863                    .entry(import_file_path.as_str())
864                    .or_default()
865                    .push((local_name.as_str(), target_id.as_str()));
866            }
867            grouped
868        } else {
869            HashMap::new()
870        };
871
872    // Pass 2: Build scopes, imports, and resolve references per file (parallel)
873    let per_file_results: Vec<(
874        Vec<(String, String, RefType)>,
875        Vec<ResolutionEntry>,
876        HashMap<String, HashSet<String>>,
877    )> = maybe_par_iter!(parsed_files)
878        .filter_map(|(file_path, content, tree)| {
879            let source = content.as_bytes();
880            let ext = file_path.rfind('.').map(|i| &file_path[i..]).unwrap_or("");
881            let config = get_language_config(ext).and_then(|c| c.scope_resolve)?;
882
883            let mut scopes: Vec<Scope> = vec![Scope {
884                parent: None,
885                defs: HashMap::new(),
886                bindings: HashSet::new(),
887                binding_rows: HashMap::new(),
888                types: HashMap::new(),
889                pending_call_types: HashMap::new(),
890                owner_id: None,
891                kind: "module",
892            }];
893
894            let mut entity_scope_map: HashMap<String, usize> = HashMap::new();
895            let mut entity_inner_scope: HashMap<String, usize> = HashMap::new();
896
897            if let Some(ranges) = entity_ranges.get(file_path.as_str()) {
898                for (_start, _end, eid) in ranges {
899                    if let Some(info) = entity_map.get(eid) {
900                        if info.parent_id.is_none() {
901                            scopes[0].defs.insert(info.name.clone(), eid.clone());
902                            entity_scope_map.insert(eid.clone(), 0);
903                        }
904                    }
905                }
906            }
907
908            let file_entities: Vec<&SemanticEntity> = entities_by_file
909                .get(file_path.as_str())
910                .map(|v| v.as_slice())
911                .unwrap_or(&[])
912                .to_vec();
913            let file_lookup = FileEntityLookup::new(&file_entities);
914            let entity_spans = find_entity_source_spans(&file_entities, content);
915
916            build_scopes_from_ast(
917                tree.root_node(),
918                0,
919                &mut scopes,
920                &mut entity_scope_map,
921                &mut entity_inner_scope,
922                &file_lookup,
923                &children_by_parent,
924                entity_map,
925                file_path,
926                source,
927                config,
928            );
929
930            let mut local_import_table: HashMap<(String, String), String> = HashMap::new();
931            if pre_built_import_table.is_some() {
932                if let Some(entries) = import_table_by_file.get(file_path.as_str()) {
933                    for (local_name, target_id) in entries {
934                        local_import_table.insert(
935                            (file_path.clone(), (*local_name).to_string()),
936                            (*target_id).to_string(),
937                        );
938                        scopes[0]
939                            .defs
940                            .insert((*local_name).to_string(), (*target_id).to_string());
941                    }
942                }
943            }
944            extract_imports_from_ast(
945                tree.root_node(),
946                file_path,
947                source,
948                &symbol_table,
949                entity_map,
950                &mut local_import_table,
951                &mut scopes,
952                config,
953                &go_pkg_index,
954                &ts_default_exports,
955                &top_level_entities,
956                parsed_files,
957                &content_by_file,
958                &exported_names_by_file,
959                pre_built_import_table.is_some(),
960            );
961
962            // The per-file import table is keyed by (file_path, name) but only ever
963            // holds this file's entries, so re-key it by name once. resolve_ref then
964            // looks up imports without allocating a key string per reference.
965            let local_import_by_name: HashMap<&str, &str> = local_import_table
966                .iter()
967                .map(|((_, name), target_id)| (name.as_str(), target_id.as_str()))
968                .collect();
969
970            // Resolve pending call types using the complete return type map.
971            inject_return_type_bindings(
972                &mut scopes,
973                &func_name_return_types,
974                &return_type_map,
975                &local_import_by_name,
976            );
977
978            let mut file_edges: Vec<(String, String, RefType)> = Vec::new();
979            let mut file_log: Vec<ResolutionEntry> = Vec::new();
980            let mut file_consumed_words: HashMap<String, HashSet<String>> = HashMap::new();
981
982            // Walk the AST once for the entire file, collecting all refs with row positions
983            let all_file_refs = collect_all_file_refs(tree.root_node(), source, config);
984            let refs_by_row = build_refs_by_row(&all_file_refs);
985            let descendant_ranges_by_entity =
986                build_descendant_ranges_by_entity(&file_entities, entity_map);
987            let mut lookup_cache = ScopeLookupCache::default();
988
989            for entity in &file_entities {
990                if emit_entity_ids
991                    .as_ref()
992                    .is_some_and(|ids| !ids.contains(entity.id.as_str()))
993                {
994                    continue;
995                }
996
997                let scope_idx = entity_inner_scope
998                    .get(&entity.id)
999                    .or_else(|| entity_scope_map.get(&entity.id))
1000                    .copied()
1001                    .unwrap_or(0);
1002
1003                let start_row = entity.start_line.saturating_sub(1).min(refs_by_row.len());
1004                let end_row = entity.end_line.min(refs_by_row.len()).max(start_row);
1005                if emit_local_binding_log {
1006                    log_scope_bindings(
1007                        &mut file_log,
1008                        &entity.id,
1009                        &scopes[scope_idx],
1010                        start_row,
1011                        end_row,
1012                        &descendant_ranges_by_entity,
1013                    );
1014                }
1015                // Hoist per-entity lookups out of the per-reference loop. Each reference
1016                // previously re-hashed the entity id against several maps (and every
1017                // child id, once per ref); on dense, deeply nested files that hashing
1018                // dominated resolution. Fetch them once per entity instead.
1019                let entity_consumed = file_consumed_words.entry(entity.id.clone()).or_default();
1020                add_local_bindings_to_consumed_words(entity_consumed, scope_idx, &scopes);
1021
1022                let entity_span = entity_spans.get(entity.id.as_str()).copied();
1023                let child_ref_checks: Vec<ChildRefCheck> = children_by_parent
1024                    .get(entity.id.as_str())
1025                    .map(|children| {
1026                        children
1027                            .iter()
1028                            .filter(|child| {
1029                                entity_creates_reference_scope(&child.entity_type)
1030                                    && child.file_path == entity.file_path
1031                            })
1032                            .map(|child| {
1033                                let span = entity_spans
1034                                    .get(child.id.as_str())
1035                                    .map(|span| (span.start_byte, span.end_byte));
1036                                (child.start_line, child.end_line, span)
1037                            })
1038                            .collect()
1039                    })
1040                    .unwrap_or_default();
1041                let entity_descendant_ranges = descendant_ranges_by_entity.get(&entity.id);
1042
1043                let allow_implicit_instance_member_receiver =
1044                    allows_implicit_instance_member_receiver(
1045                        file_path,
1046                        &entity.entity_type,
1047                        &entity.content,
1048                    );
1049
1050                // Filter pre-collected refs to this entity's line range
1051                for row_refs in &refs_by_row[start_row..end_row] {
1052                    for &ref_idx in row_refs {
1053                        let ast_ref = &all_file_refs[ref_idx];
1054                        if !ref_owned_by_entity(ast_ref, entity_span, &child_ref_checks) {
1055                            continue;
1056                        }
1057                        if row_in_descendant_ranges(entity_descendant_ranges, ast_ref.row) {
1058                            continue;
1059                        }
1060                        // Skip self-name refs (was previously done during collection)
1061                        let is_self_ref = match &ast_ref.kind {
1062                            AstRefKind::Call { name, .. } => name == &entity.name,
1063                            AstRefKind::ScopedCall { .. } => false,
1064                            AstRefKind::MethodCall { .. } => false,
1065                        };
1066                        if is_self_ref {
1067                            continue;
1068                        }
1069
1070                        // Languages without per-symbol imports (e.g. Swift, Kotlin)
1071                        // allow cross-file resolution for lowercase function names.
1072                        let allow_cross_file = config.import_extractor.is_none();
1073                        let resolution = resolve_ref(
1074                            ast_ref,
1075                            scope_idx,
1076                            &scopes,
1077                            &symbol_table,
1078                            &class_members,
1079                            &owner_members,
1080                            &local_import_by_name,
1081                            &instance_attr_types,
1082                            entity_map,
1083                            &swift_call_signatures,
1084                            file_path,
1085                            &entity.id,
1086                            allow_cross_file,
1087                            allow_implicit_instance_member_receiver,
1088                            &file_lookup,
1089                            &mut lookup_cache,
1090                        );
1091
1092                        if let Some((target_id, ref_type, method)) = resolution {
1093                            if target_id != entity.id {
1094                                let is_parent_child =
1095                                    entity.parent_id.as_ref().map_or(false, |pid| {
1096                                        pid == &target_id
1097                                            || entity_map.get(&target_id).map_or(false, |t| {
1098                                                t.parent_id.as_ref() == Some(&entity.id)
1099                                            })
1100                                    });
1101
1102                                if !is_parent_child {
1103                                    let reference = ref_description(ast_ref);
1104                                    file_edges.push((
1105                                        entity.id.clone(),
1106                                        target_id.clone(),
1107                                        ref_type,
1108                                    ));
1109                                    add_scope_reference_words(entity_consumed, &reference);
1110                                    file_log.push(ResolutionEntry {
1111                                        from_entity: entity.id.clone(),
1112                                        reference,
1113                                        resolved_to: Some(target_id),
1114                                        method,
1115                                    });
1116                                }
1117                            }
1118                        } else {
1119                            let reference = ref_description(ast_ref);
1120                            add_scope_reference_words(entity_consumed, &reference);
1121                            file_log.push(ResolutionEntry {
1122                                from_entity: entity.id.clone(),
1123                                reference,
1124                                resolved_to: None,
1125                                method: "unresolved",
1126                            });
1127                        }
1128                    }
1129                }
1130            }
1131
1132            Some((file_edges, file_log, file_consumed_words))
1133        })
1134        .collect();
1135
1136    for (file_edges, file_log, file_consumed_words) in per_file_results {
1137        all_edges.extend(file_edges);
1138        log.extend(file_log);
1139        for (entity_id, words) in file_consumed_words {
1140            consumed_words.entry(entity_id).or_default().extend(words);
1141        }
1142    }
1143
1144    // Deduplicate edges
1145    let mut seen: std::collections::HashSet<(String, String)> =
1146        std::collections::HashSet::with_capacity(all_edges.len());
1147    let deduped_edges: Vec<(String, String, RefType)> = {
1148        let mut result = Vec::with_capacity(all_edges.len());
1149        for edge in all_edges {
1150            if seen.insert((edge.0.clone(), edge.1.clone())) {
1151                result.push(edge);
1152            }
1153        }
1154        result
1155    };
1156    let all_edges = deduped_edges;
1157
1158    ScopeResultFull {
1159        edges: all_edges,
1160        resolution_log: log,
1161        consumed_words,
1162    }
1163}
1164
1165fn ref_description(ast_ref: &AstRef) -> String {
1166    match &ast_ref.kind {
1167        AstRefKind::Call {
1168            name,
1169            argument_labels,
1170        } => format!(
1171            "{}({})",
1172            name,
1173            format_argument_labels(argument_labels.as_deref())
1174        ),
1175        AstRefKind::ScopedCall { path, name } => format!("{}::{}()", path, name),
1176        AstRefKind::MethodCall {
1177            receiver,
1178            method,
1179            argument_labels,
1180        } => format!(
1181            "{}.{}({})",
1182            receiver,
1183            method,
1184            format_argument_labels(argument_labels.as_deref())
1185        ),
1186    }
1187}
1188
1189fn format_argument_labels(argument_labels: Option<&[Option<String>]>) -> String {
1190    argument_labels
1191        .map(|labels| {
1192            labels
1193                .iter()
1194                .map(|label| {
1195                    label
1196                        .as_deref()
1197                        .map_or("_:".to_string(), |label| format!("{label}:"))
1198                })
1199                .collect::<Vec<_>>()
1200                .join(", ")
1201        })
1202        .unwrap_or_default()
1203}
1204
1205fn add_scope_reference_words(words: &mut HashSet<String>, reference: &str) {
1206    let reference = reference.strip_suffix("()").unwrap_or(reference);
1207    let reference = reference
1208        .split_once('(')
1209        .map_or(reference, |(name, _)| name);
1210    if let Some((receiver, member)) = reference.rsplit_once('.') {
1211        if !receiver.is_empty() {
1212            words.insert(receiver.to_string());
1213        }
1214        if !member.is_empty() {
1215            words.insert(member.to_string());
1216        }
1217    } else if reference.contains("::") {
1218        for part in reference.split("::").filter(|part| !part.is_empty()) {
1219            words.insert(part.to_string());
1220        }
1221    } else if !reference.is_empty() {
1222        words.insert(reference.to_string());
1223    }
1224}
1225
1226fn add_local_bindings_to_consumed_words(
1227    words: &mut HashSet<String>,
1228    start_scope: usize,
1229    scopes: &[Scope],
1230) {
1231    let mut idx = Some(start_scope);
1232    while let Some(scope_idx) = idx {
1233        words.extend(scopes[scope_idx].bindings.iter().cloned());
1234        idx = scopes[scope_idx].parent;
1235    }
1236}
1237
1238fn log_scope_bindings(
1239    file_log: &mut Vec<ResolutionEntry>,
1240    from_entity: &str,
1241    scope: &Scope,
1242    start_row: usize,
1243    end_row: usize,
1244    descendant_ranges_by_entity: &HashMap<String, Vec<(usize, usize)>>,
1245) {
1246    let mut bindings: Vec<&String> = scope.bindings.iter().collect();
1247    bindings.sort();
1248    for binding in bindings {
1249        let belongs_to_entity = scope.binding_rows.get(binding).map_or(false, |rows| {
1250            rows.iter().any(|row| {
1251                *row >= start_row
1252                    && *row < end_row
1253                    && !row_belongs_to_descendant(descendant_ranges_by_entity, from_entity, *row)
1254            })
1255        });
1256        if !belongs_to_entity {
1257            continue;
1258        }
1259        file_log.push(ResolutionEntry {
1260            from_entity: from_entity.to_string(),
1261            reference: binding.clone(),
1262            resolved_to: None,
1263            method: "local_binding",
1264        });
1265    }
1266}
1267
1268fn build_descendant_ranges_by_entity(
1269    file_entities: &[&SemanticEntity],
1270    entity_map: &HashMap<String, EntityInfo>,
1271) -> HashMap<String, Vec<(usize, usize)>> {
1272    let mut ranges_by_entity: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
1273    let mut sorted_entities = file_entities.to_vec();
1274    sorted_entities.sort_by(|left, right| {
1275        left.start_line
1276            .cmp(&right.start_line)
1277            .then_with(|| right.end_line.cmp(&left.end_line))
1278            .then_with(|| left.id.cmp(&right.id))
1279    });
1280
1281    let mut ancestor_stack: Vec<&SemanticEntity> = Vec::new();
1282    for entity in sorted_entities {
1283        while ancestor_stack.last().map_or(false, |candidate| {
1284            !is_strict_enclosing_range(candidate, entity)
1285        }) {
1286            ancestor_stack.pop();
1287        }
1288
1289        if !entity_creates_reference_scope(&entity.entity_type) {
1290            ancestor_stack.push(entity);
1291            continue;
1292        }
1293
1294        let child_range = (entity.start_line.saturating_sub(1), entity.end_line);
1295        let mut current = entity.parent_id.as_deref();
1296        let mut visited = HashSet::new();
1297        while let Some(parent_id) = current {
1298            if !visited.insert(parent_id.to_string()) {
1299                break;
1300            }
1301            ranges_by_entity
1302                .entry(parent_id.to_string())
1303                .or_default()
1304                .push(child_range);
1305            current = entity_map
1306                .get(parent_id)
1307                .and_then(|parent| parent.parent_id.as_deref());
1308        }
1309
1310        for ancestor in &ancestor_stack {
1311            ranges_by_entity
1312                .entry(ancestor.id.clone())
1313                .or_default()
1314                .push(child_range);
1315        }
1316
1317        ancestor_stack.push(entity);
1318    }
1319    for ranges in ranges_by_entity.values_mut() {
1320        ranges.sort_unstable();
1321        ranges.dedup();
1322    }
1323    ranges_by_entity
1324}
1325
1326fn is_strict_enclosing_range(candidate: &SemanticEntity, child: &SemanticEntity) -> bool {
1327    candidate.file_path == child.file_path
1328        && candidate.start_line <= child.start_line
1329        && child.end_line <= candidate.end_line
1330        && (candidate.start_line < child.start_line || child.end_line < candidate.end_line)
1331}
1332
1333fn row_belongs_to_descendant(
1334    descendant_ranges_by_entity: &HashMap<String, Vec<(usize, usize)>>,
1335    entity_id: &str,
1336    row: usize,
1337) -> bool {
1338    row_in_descendant_ranges(descendant_ranges_by_entity.get(entity_id), row)
1339}
1340
1341/// Same check as [`row_belongs_to_descendant`], but over pre-fetched ranges so the
1342/// per-ref loop avoids a HashMap lookup per reference.
1343fn row_in_descendant_ranges(ranges: Option<&Vec<(usize, usize)>>, row: usize) -> bool {
1344    ranges.map_or(false, |ranges| {
1345        let eligible = ranges.partition_point(|(start, _)| *start <= row);
1346        ranges[..eligible]
1347            .iter()
1348            .rev()
1349            .any(|(start, end)| row >= *start && row < *end)
1350    })
1351}
1352
1353/// Build scope tree by walking the AST.
1354/// Creates class scopes and maps methods to them.
1355/// Uses an iterative worklist to avoid stack overflow on deeply nested ASTs.
1356/// Fixes: https://github.com/Ataraxy-Labs/sem/issues/103
1357fn push_named_children_rev<'a>(
1358    worklist: &mut Vec<tree_sitter::Node<'a>>,
1359    node: tree_sitter::Node<'a>,
1360) {
1361    for idx in (0..node.named_child_count()).rev() {
1362        if let Some(child) = node.named_child(idx as u32) {
1363            worklist.push(child);
1364        }
1365    }
1366}
1367
1368fn push_scoped_named_children_rev<'a>(
1369    worklist: &mut Vec<(tree_sitter::Node<'a>, usize)>,
1370    node: tree_sitter::Node<'a>,
1371    scope: usize,
1372) {
1373    for idx in (0..node.named_child_count()).rev() {
1374        if let Some(child) = node.named_child(idx as u32) {
1375            worklist.push((child, scope));
1376        }
1377    }
1378}
1379
1380fn build_scopes_from_ast(
1381    root: tree_sitter::Node,
1382    root_scope: usize,
1383    scopes: &mut Vec<Scope>,
1384    entity_scope_map: &mut HashMap<String, usize>,
1385    entity_inner_scope: &mut HashMap<String, usize>,
1386    file_lookup: &FileEntityLookup<'_>,
1387    children_by_parent: &HashMap<&str, Vec<&SemanticEntity>>,
1388    entity_map: &HashMap<String, EntityInfo>,
1389    _file_path: &str,
1390    source: &[u8],
1391    config: &ScopeResolveConfig,
1392) {
1393    // Each entry: (node, current_scope)
1394    let mut worklist: Vec<(tree_sitter::Node, usize)> = vec![(root, root_scope)];
1395
1396    while let Some((node, current_scope)) = worklist.pop() {
1397        let kind = node.kind();
1398
1399        // Class-like scope: config-driven
1400        let is_class_like = config.class_scope_nodes.contains(&kind);
1401
1402        // Impl scope: config-driven (Rust impl_item, Swift extension)
1403        let is_impl = config.impl_scope_nodes.contains(&kind);
1404
1405        if is_class_like || is_impl {
1406            let class_name = if is_impl {
1407                node.child_by_field_name("type")
1408                    .and_then(|n| n.utf8_text(source).ok())
1409                    .unwrap_or("")
1410            } else {
1411                match &config.class_name_field {
1412                    ClassNameField::Simple(field) => node
1413                        .child_by_field_name(field)
1414                        .and_then(|n| n.utf8_text(source).ok())
1415                        .unwrap_or(""),
1416                    ClassNameField::TypeSpec { spec_kind, field } => {
1417                        let mut name = "";
1418                        let mut cursor = node.walk();
1419                        for child in node.named_children(&mut cursor) {
1420                            if child.kind() == *spec_kind {
1421                                name = child
1422                                    .child_by_field_name(field)
1423                                    .and_then(|n| n.utf8_text(source).ok())
1424                                    .unwrap_or("");
1425                                break;
1426                            }
1427                        }
1428                        name
1429                    }
1430                    ClassNameField::ImplType(field) => node
1431                        .child_by_field_name(field)
1432                        .and_then(|n| n.utf8_text(source).ok())
1433                        .unwrap_or(""),
1434                }
1435            };
1436
1437            let line = node.start_position().row + 1;
1438            let class_entity = file_lookup.find_at_line(class_name, line, |entity| {
1439                matches!(
1440                    entity.entity_type.as_str(),
1441                    "class"
1442                        | "struct"
1443                        | "interface"
1444                        | "enum"
1445                        | "protocol"
1446                        | "protocol_declaration"
1447                        | "object_declaration"
1448                        | "companion_object"
1449                )
1450            });
1451
1452            if let Some(ce) = class_entity {
1453                let existing_scope = entity_inner_scope.get(&ce.id).copied();
1454
1455                let class_scope_idx = if let Some(idx) = existing_scope {
1456                    idx
1457                } else {
1458                    let idx = scopes.len();
1459                    scopes.push(Scope {
1460                        parent: Some(current_scope),
1461                        defs: HashMap::new(),
1462                        bindings: HashSet::new(),
1463                        binding_rows: HashMap::new(),
1464                        types: HashMap::new(),
1465                        pending_call_types: HashMap::new(),
1466                        owner_id: Some(ce.id.clone()),
1467                        kind: "class",
1468                    });
1469                    entity_scope_map.insert(ce.id.clone(), current_scope);
1470                    entity_inner_scope.insert(ce.id.clone(), idx);
1471                    idx
1472                };
1473
1474                if let Some(children) = children_by_parent.get(ce.id.as_str()) {
1475                    for entity in children {
1476                        scopes[class_scope_idx]
1477                            .defs
1478                            .insert(entity.name.clone(), entity.id.clone());
1479                        entity_scope_map.insert(entity.id.clone(), class_scope_idx);
1480                    }
1481                }
1482
1483                push_scoped_named_children_rev(&mut worklist, node, class_scope_idx);
1484                continue;
1485            } else if !is_impl {
1486                let class_scope_idx = scopes.len();
1487                scopes.push(Scope {
1488                    parent: Some(current_scope),
1489                    defs: HashMap::new(),
1490                    bindings: HashSet::new(),
1491                    binding_rows: HashMap::new(),
1492                    types: HashMap::new(),
1493                    pending_call_types: HashMap::new(),
1494                    owner_id: None,
1495                    kind: "class",
1496                });
1497                push_scoped_named_children_rev(&mut worklist, node, class_scope_idx);
1498                continue;
1499            }
1500        }
1501
1502        // Rust mod_item: create a module scope so nested functions resolve
1503        // names from the parent scope (e.g. super::target() walks up correctly).
1504        if kind == "mod_item" {
1505            let mod_name = node
1506                .child_by_field_name("name")
1507                .and_then(|n| n.utf8_text(source).ok())
1508                .unwrap_or("");
1509            let mod_scope_idx = scopes.len();
1510            scopes.push(Scope {
1511                parent: Some(current_scope),
1512                defs: HashMap::new(),
1513                bindings: HashSet::new(),
1514                binding_rows: HashMap::new(),
1515                types: HashMap::new(),
1516                pending_call_types: HashMap::new(),
1517                owner_id: None,
1518                kind: "module",
1519            });
1520
1521            // Register any entities that are children of this module
1522            let line = node.start_position().row + 1;
1523            let mod_entity =
1524                file_lookup.find_at_line(mod_name, line, |entity| entity.entity_type == "module");
1525
1526            if let Some(me) = mod_entity {
1527                scopes[mod_scope_idx].owner_id = Some(me.id.clone());
1528                entity_scope_map
1529                    .entry(me.id.clone())
1530                    .or_insert(current_scope);
1531                entity_inner_scope.insert(me.id.clone(), mod_scope_idx);
1532
1533                // Register child entities in the module scope
1534                if let Some(children) = children_by_parent.get(me.id.as_str()) {
1535                    for child_entity in children {
1536                        scopes[mod_scope_idx]
1537                            .defs
1538                            .insert(child_entity.name.clone(), child_entity.id.clone());
1539                        entity_scope_map.insert(child_entity.id.clone(), mod_scope_idx);
1540                    }
1541                }
1542            }
1543
1544            push_scoped_named_children_rev(&mut worklist, node, mod_scope_idx);
1545            continue;
1546        }
1547
1548        // Function-like scope: config-driven
1549        let is_function_like = config.function_scope_nodes.contains(&kind);
1550
1551        if is_function_like {
1552            let func_name = node
1553                .child_by_field_name("name")
1554                .and_then(|n| n.utf8_text(source).ok())
1555                .unwrap_or("");
1556
1557            let parent_scope = if config.external_method && kind == "method_declaration" {
1558                let receiver_type = node
1559                    .utf8_text(source)
1560                    .ok()
1561                    .and_then(|t| extract_go_receiver_type(t));
1562                if let Some(ref struct_name) = receiver_type {
1563                    let found = scopes.iter().enumerate().find(|(_, s)| {
1564                        s.kind == "class"
1565                            && s.owner_id.as_ref().map_or(false, |oid| {
1566                                entity_map
1567                                    .get(oid)
1568                                    .map_or(false, |e| e.name == *struct_name)
1569                            })
1570                    });
1571                    found.map(|(idx, _)| idx).unwrap_or(current_scope)
1572                } else {
1573                    current_scope
1574                }
1575            } else {
1576                current_scope
1577            };
1578
1579            let func_scope_idx = scopes.len();
1580            scopes.push(Scope {
1581                parent: Some(parent_scope),
1582                defs: HashMap::new(),
1583                bindings: HashSet::new(),
1584                binding_rows: HashMap::new(),
1585                types: HashMap::new(),
1586                pending_call_types: HashMap::new(),
1587                owner_id: None,
1588                kind: "function",
1589            });
1590
1591            let line = node.start_position().row + 1;
1592            let func_entity = file_lookup.find_at_line(func_name, line, |_| true);
1593
1594            if let Some(fe) = func_entity {
1595                scopes[func_scope_idx].owner_id = Some(fe.id.clone());
1596                entity_scope_map
1597                    .entry(fe.id.clone())
1598                    .or_insert(parent_scope);
1599                entity_inner_scope.insert(fe.id.clone(), func_scope_idx);
1600                if config.external_method
1601                    && kind == "method_declaration"
1602                    && parent_scope != current_scope
1603                {
1604                    scopes[parent_scope]
1605                        .defs
1606                        .insert(fe.name.clone(), fe.id.clone());
1607                }
1608            }
1609
1610            scan_assignments(node, func_scope_idx, scopes, source, config);
1611            scan_function_params(node, func_scope_idx, scopes, source, config);
1612
1613            if config.external_method && kind == "method_declaration" {
1614                if let Some(receiver) = node.child_by_field_name("receiver") {
1615                    let mut rcursor = receiver.walk();
1616                    for param in receiver.named_children(&mut rcursor) {
1617                        if param.kind() == "parameter_declaration" {
1618                            let param_name = param
1619                                .child_by_field_name("name")
1620                                .and_then(|n| n.utf8_text(source).ok())
1621                                .unwrap_or("");
1622                            let param_type = param
1623                                .child_by_field_name("type")
1624                                .map(|n| extract_base_type(n, source))
1625                                .unwrap_or_default();
1626                            if !param_name.is_empty() && !param_type.is_empty() {
1627                                scopes[func_scope_idx]
1628                                    .types
1629                                    .insert(param_name.to_string(), param_type);
1630                            }
1631                        }
1632                    }
1633                }
1634            }
1635
1636            push_scoped_named_children_rev(&mut worklist, node, func_scope_idx);
1637            continue;
1638        }
1639
1640        push_scoped_named_children_rev(&mut worklist, node, current_scope);
1641    }
1642}
1643
1644/// Scan for variable assignments and record type bindings.
1645fn scan_assignments(
1646    root: tree_sitter::Node,
1647    scope_idx: usize,
1648    scopes: &mut Vec<Scope>,
1649    source: &[u8],
1650    config: &ScopeResolveConfig,
1651) {
1652    let mut worklist = vec![root];
1653    while let Some(node) = worklist.pop() {
1654        let mut cursor = node.walk();
1655        for child in node.named_children(&mut cursor) {
1656            let ck = child.kind();
1657
1658            // Check if this node matches an assignment rule
1659            for rule in config.assignment_rules {
1660                if ck == rule.node_kind {
1661                    match rule.strategy {
1662                        AssignmentStrategy::LeftRight => {
1663                            scan_single_assignment(child, scope_idx, scopes, source);
1664                        }
1665                        AssignmentStrategy::Declarators => {
1666                            scan_ts_var_declaration(child, scope_idx, scopes, source);
1667                        }
1668                        AssignmentStrategy::PatternBased => {
1669                            scan_rust_let_declaration(child, scope_idx, scopes, source);
1670                        }
1671                        AssignmentStrategy::ShortVar => {
1672                            scan_go_short_var(child, scope_idx, scopes, source);
1673                        }
1674                        AssignmentStrategy::VarSpec => {
1675                            scan_go_var_declaration(child, scope_idx, scopes, source);
1676                        }
1677                    }
1678                }
1679            }
1680
1681            // Recurse into configured container nodes
1682            if config.assignment_recurse_into.contains(&ck) {
1683                worklist.push(child);
1684            }
1685        }
1686    }
1687}
1688
1689fn record_binding(scopes: &mut [Scope], scope_idx: usize, name: &str, row: usize) {
1690    scopes[scope_idx].bindings.insert(name.to_string());
1691    scopes[scope_idx]
1692        .binding_rows
1693        .entry(name.to_string())
1694        .or_default()
1695        .push(row);
1696}
1697
1698/// Scan function parameter type annotations and add them as type bindings.
1699/// e.g. `def foo(shelter: Shelter)` -> types["shelter"] = "Shelter"
1700fn scan_function_params(
1701    node: tree_sitter::Node,
1702    scope_idx: usize,
1703    scopes: &mut Vec<Scope>,
1704    source: &[u8],
1705    config: &ScopeResolveConfig,
1706) {
1707    // Try "parameters" field first (Python, TS, Rust, Go, etc.)
1708    // Fallback to direct children for languages like Swift where
1709    // params are direct children of function_declaration.
1710    let mut params_node = node.child_by_field_name("parameters");
1711    if params_node.is_none() {
1712        // Kotlin: function_value_parameters
1713        let mut c = node.walk();
1714        for ch in node.named_children(&mut c) {
1715            if ch.kind() == "function_value_parameters" {
1716                params_node = Some(ch);
1717                break;
1718            }
1719        }
1720    }
1721
1722    // If we have a params container, iterate its children.
1723    // Otherwise, iterate direct children of the function node (Swift).
1724    let (iter_node, use_direct) = match params_node {
1725        Some(p) => (p, false),
1726        None => (node, true),
1727    };
1728
1729    let mut cursor = iter_node.walk();
1730    for child in iter_node.named_children(&mut cursor) {
1731        // When using direct children, only process param-like nodes
1732        if use_direct {
1733            let is_param = config
1734                .param_rules
1735                .iter()
1736                .any(|r| child.kind() == r.node_kind);
1737            if !is_param {
1738                continue;
1739            }
1740        }
1741        for rule in config.param_rules {
1742            if child.kind() != rule.node_kind {
1743                continue;
1744            }
1745
1746            let param_name = match &rule.name_field {
1747                ParamNameField::Simple(field) => child
1748                    .child_by_field_name(field)
1749                    .and_then(|n| n.utf8_text(source).ok())
1750                    .unwrap_or(""),
1751                ParamNameField::WithFallback(field) => child
1752                    .child_by_field_name(field)
1753                    .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
1754                    .and_then(|n| n.utf8_text(source).ok())
1755                    .unwrap_or(""),
1756                ParamNameField::RustPattern => child
1757                    .child_by_field_name("pattern")
1758                    .and_then(|n| {
1759                        if n.kind() == "identifier" {
1760                            n.utf8_text(source).ok()
1761                        } else if n.kind() == "mut_pattern" {
1762                            n.named_child(0).and_then(|c| c.utf8_text(source).ok())
1763                        } else if n.kind() == "reference_pattern" {
1764                            n.named_child(0).and_then(|c| {
1765                                if c.kind() == "identifier" {
1766                                    c.utf8_text(source).ok()
1767                                } else if c.kind() == "mut_pattern" {
1768                                    c.named_child(0).and_then(|cc| cc.utf8_text(source).ok())
1769                                } else {
1770                                    None
1771                                }
1772                            })
1773                        } else {
1774                            None
1775                        }
1776                    })
1777                    .unwrap_or(""),
1778            };
1779
1780            if param_name.is_empty() || rule.skip_names.contains(&param_name) {
1781                continue;
1782            }
1783            record_binding(scopes, scope_idx, param_name, child.start_position().row);
1784
1785            // Try the configured type field first, then fall back to child type nodes
1786            // (Swift parameters have user_type children instead of a "type" field)
1787            let mut type_node = child.child_by_field_name(rule.type_field);
1788            if type_node.is_none() {
1789                let mut tc = child.walk();
1790                for ch in child.named_children(&mut tc) {
1791                    if matches!(
1792                        ch.kind(),
1793                        "user_type" | "type_annotation" | "type_identifier"
1794                    ) {
1795                        type_node = Some(ch);
1796                        break;
1797                    }
1798                }
1799            }
1800            if let Some(tn) = type_node {
1801                let type_text = extract_base_type(tn, source);
1802                if !type_text.is_empty()
1803                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1804                {
1805                    scopes[scope_idx]
1806                        .types
1807                        .insert(param_name.to_string(), type_text);
1808                }
1809            }
1810        }
1811    }
1812}
1813
1814/// Python/TS: `x = Foo()` or `x = func()`
1815fn scan_single_assignment(
1816    node: tree_sitter::Node,
1817    scope_idx: usize,
1818    scopes: &mut Vec<Scope>,
1819    source: &[u8],
1820) {
1821    let assign = if node.kind() == "assignment" {
1822        node
1823    } else {
1824        let mut cursor = node.walk();
1825        let children: Vec<_> = node.named_children(&mut cursor).collect();
1826        match children
1827            .into_iter()
1828            .find(|c| c.kind() == "assignment" || c.kind() == "assignment_expression")
1829        {
1830            Some(a) => a,
1831            None => return,
1832        }
1833    };
1834
1835    let left = match assign.child_by_field_name("left") {
1836        Some(l) => l,
1837        None => return,
1838    };
1839    let right = match assign.child_by_field_name("right") {
1840        Some(r) => r,
1841        None => return,
1842    };
1843
1844    if left.kind() != "identifier" {
1845        return;
1846    }
1847    let var_name = match left.utf8_text(source) {
1848        Ok(n) => n.to_string(),
1849        Err(_) => return,
1850    };
1851    record_binding(scopes, scope_idx, &var_name, left.start_position().row);
1852
1853    record_type_from_rhs(right, &var_name, scope_idx, scopes, source);
1854}
1855
1856/// TS: `const x = new Foo()` or `const x: Type = ...` or `const x = func()`
1857/// Also handles Swift `let x = Foo(...)` and Kotlin `val x = Foo(...)`
1858fn scan_ts_var_declaration(
1859    node: tree_sitter::Node,
1860    scope_idx: usize,
1861    scopes: &mut Vec<Scope>,
1862    source: &[u8],
1863) {
1864    let mut cursor = node.walk();
1865    for child in node.named_children(&mut cursor) {
1866        if child.kind() == "variable_declarator" {
1867            let var_name = child
1868                .child_by_field_name("name")
1869                .and_then(|n| n.utf8_text(source).ok())
1870                .unwrap_or("")
1871                .to_string();
1872            if var_name.is_empty() {
1873                continue;
1874            }
1875            let binding_row = child
1876                .child_by_field_name("name")
1877                .map(|n| n.start_position().row)
1878                .unwrap_or_else(|| child.start_position().row);
1879            record_binding(scopes, scope_idx, &var_name, binding_row);
1880
1881            // Check for explicit type annotation: `const x: Foo = ...`
1882            if let Some(type_ann) = child.child_by_field_name("type") {
1883                let type_text = extract_base_type(type_ann, source);
1884                if !type_text.is_empty()
1885                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1886                {
1887                    scopes[scope_idx].types.insert(var_name.clone(), type_text);
1888                    continue;
1889                }
1890            }
1891
1892            // Check RHS value
1893            if let Some(value) = child.child_by_field_name("value") {
1894                record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
1895            }
1896        }
1897    }
1898
1899    if node.kind() == "property_declaration" {
1900        let var_names = swift_property_declaration_names(node, source);
1901
1902        if !var_names.is_empty() {
1903            if let Some(name_nodes) = swift_property_declaration_name_nodes(node) {
1904                for (idx, var_name) in var_names.iter().enumerate() {
1905                    let binding_row = name_nodes
1906                        .get(idx)
1907                        .map(|name_node| name_node.start_position().row)
1908                        .unwrap_or_else(|| node.start_position().row);
1909                    record_binding(scopes, scope_idx, var_name, binding_row);
1910                }
1911
1912                let type_names: Vec<Option<String>> = name_nodes
1913                    .iter()
1914                    .enumerate()
1915                    .map(|(idx, name_node)| {
1916                        swift_property_type_for_name(node, *name_node, idx, source)
1917                    })
1918                    .collect();
1919                for (idx, name_node) in name_nodes.iter().enumerate() {
1920                    let Some(var_name) = var_names.get(idx) else {
1921                        continue;
1922                    };
1923                    let type_name =
1924                        type_names
1925                            .get(idx)
1926                            .and_then(|name| name.clone())
1927                            .or_else(|| {
1928                                if type_names[..idx].iter().any(Option::is_some) {
1929                                    None
1930                                } else {
1931                                    type_names
1932                                        .iter()
1933                                        .skip(idx + 1)
1934                                        .find_map(|name| name.clone())
1935                                }
1936                            });
1937                    if let Some(type_name) = type_name {
1938                        if !type_name.is_empty()
1939                            && type_name.chars().next().map_or(false, |c| c.is_uppercase())
1940                        {
1941                            scopes[scope_idx].types.insert(var_name.clone(), type_name);
1942                            continue;
1943                        }
1944                    }
1945                    if let Some(value) =
1946                        swift_property_value_for_name(node, *name_node, idx, source)
1947                    {
1948                        record_type_from_rhs(value, var_name, scope_idx, scopes, source);
1949                    }
1950                }
1951            } else if let Some(var_name) = var_names.first() {
1952                record_binding(scopes, scope_idx, var_name, node.start_position().row);
1953
1954                if let Some(type_ann) = node.child_by_field_name("type") {
1955                    let type_text = extract_base_type(type_ann, source);
1956                    if !type_text.is_empty()
1957                        && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1958                    {
1959                        scopes[scope_idx].types.insert(var_name.clone(), type_text);
1960                        return;
1961                    }
1962                }
1963                if let Some(value) = node.child_by_field_name("value") {
1964                    record_type_from_rhs(value, var_name, scope_idx, scopes, source);
1965                } else {
1966                    let mut c = node.walk();
1967                    for ch in node.named_children(&mut c) {
1968                        if ch.kind() == "call_expression" || ch.kind() == "new_expression" {
1969                            record_type_from_rhs(ch, var_name, scope_idx, scopes, source);
1970                            break;
1971                        }
1972                    }
1973                }
1974            }
1975            return;
1976        }
1977
1978        // Kotlin: property_declaration > variable_declaration > identifier, then sibling call_expression
1979        let mut c = node.walk();
1980        for child in node.named_children(&mut c) {
1981            if child.kind() == "variable_declaration" {
1982                let var_name_kt = child
1983                    .child_by_field_name("name")
1984                    .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier"))
1985                    .and_then(|n| n.utf8_text(source).ok())
1986                    .unwrap_or("")
1987                    .to_string();
1988
1989                if !var_name_kt.is_empty() {
1990                    // Check for type annotation on the property_declaration
1991                    if let Some(type_ann) = node.child_by_field_name("type") {
1992                        let type_text = extract_base_type(type_ann, source);
1993                        if !type_text.is_empty()
1994                            && type_text.chars().next().map_or(false, |c| c.is_uppercase())
1995                        {
1996                            scopes[scope_idx]
1997                                .types
1998                                .insert(var_name_kt.clone(), type_text);
1999                            return;
2000                        }
2001                    }
2002                    // Find the value (sibling call_expression or other expression)
2003                    let mut c2 = node.walk();
2004                    for sibling in node.named_children(&mut c2) {
2005                        if sibling.kind() == "call_expression" || sibling.kind() == "new_expression"
2006                        {
2007                            record_type_from_rhs(sibling, &var_name_kt, scope_idx, scopes, source);
2008                            break;
2009                        }
2010                    }
2011                }
2012                break;
2013            }
2014        }
2015    }
2016}
2017
2018fn swift_property_declaration_names(node: tree_sitter::Node, source: &[u8]) -> Vec<String> {
2019    let mut names = Vec::new();
2020    for index in 0..node.child_count() {
2021        if node.field_name_for_child(index as u32) == Some("name") {
2022            if let Some(child) = node.child(index as u32) {
2023                if let Ok(name) = child.utf8_text(source) {
2024                    if !name.is_empty() {
2025                        names.push(name.to_string());
2026                    }
2027                }
2028            }
2029        }
2030    }
2031
2032    if !names.is_empty() {
2033        return names;
2034    }
2035
2036    let mut cursor = node.walk();
2037    for child in node.named_children(&mut cursor) {
2038        if child.kind() != "pattern" {
2039            continue;
2040        }
2041        if let Some(id) = child.named_child(0) {
2042            if id.kind() == "simple_identifier" || id.kind() == "identifier" {
2043                if let Ok(name) = id.utf8_text(source) {
2044                    if !name.is_empty() {
2045                        names.push(name.to_string());
2046                    }
2047                }
2048            }
2049        }
2050    }
2051
2052    names
2053}
2054
2055fn swift_property_declaration_name_nodes<'a>(
2056    node: tree_sitter::Node<'a>,
2057) -> Option<Vec<tree_sitter::Node<'a>>> {
2058    let mut nodes = Vec::new();
2059    for index in 0..node.child_count() {
2060        if node.field_name_for_child(index as u32) == Some("name") {
2061            if let Some(child) = node.child(index as u32) {
2062                nodes.push(child);
2063            }
2064        }
2065    }
2066    if nodes.is_empty() {
2067        None
2068    } else {
2069        Some(nodes)
2070    }
2071}
2072
2073fn swift_property_value_for_name<'a>(
2074    node: tree_sitter::Node<'a>,
2075    name_node: tree_sitter::Node<'a>,
2076    name_index: usize,
2077    source: &[u8],
2078) -> Option<tree_sitter::Node<'a>> {
2079    let segment_end = swift_property_segment_end_for_name(node, name_node, name_index);
2080
2081    for child_index in 0..node.child_count() {
2082        let Some(child) = node.child(child_index as u32) else {
2083            continue;
2084        };
2085        if child.start_byte() < name_node.end_byte() || child.start_byte() >= segment_end {
2086            continue;
2087        }
2088        let field_name = node.field_name_for_child(child_index as u32);
2089        if matches!(field_name, Some("value") | Some("computed_value"))
2090            || child.kind() == "call_expression"
2091            || child.kind() == "new_expression"
2092        {
2093            return Some(child);
2094        }
2095    }
2096
2097    let segment = source
2098        .get(name_node.end_byte()..segment_end)
2099        .and_then(|bytes| std::str::from_utf8(bytes).ok())
2100        .unwrap_or("");
2101    if segment.contains('=') {
2102        let mut cursor = node.walk();
2103        for child in node.named_children(&mut cursor) {
2104            if child.start_byte() >= name_node.end_byte()
2105                && child.start_byte() < segment_end
2106                && (child.kind() == "call_expression" || child.kind() == "new_expression")
2107            {
2108                return Some(child);
2109            }
2110        }
2111    }
2112
2113    None
2114}
2115
2116fn swift_property_type_for_name(
2117    node: tree_sitter::Node,
2118    name_node: tree_sitter::Node,
2119    name_index: usize,
2120    source: &[u8],
2121) -> Option<String> {
2122    let segment_end = swift_property_segment_end_for_name(node, name_node, name_index);
2123    for child_index in 0..node.child_count() {
2124        let Some(child) = node.child(child_index as u32) else {
2125            continue;
2126        };
2127        if child.start_byte() < name_node.end_byte() || child.start_byte() >= segment_end {
2128            continue;
2129        }
2130        let field_name = node.field_name_for_child(child_index as u32);
2131        if field_name == Some("type") || child.kind() == "type_annotation" {
2132            let type_text = extract_base_type(child, source);
2133            if !type_text.is_empty() {
2134                return Some(type_text);
2135            }
2136        }
2137    }
2138    None
2139}
2140
2141fn swift_property_segment_end_for_name(
2142    node: tree_sitter::Node,
2143    name_node: tree_sitter::Node,
2144    name_index: usize,
2145) -> usize {
2146    let name_nodes = swift_property_declaration_name_nodes(node).unwrap_or_default();
2147    let next_name_start = name_nodes.get(name_index + 1).map(|next| next.start_byte());
2148    let mut segment_end = next_name_start.unwrap_or_else(|| node.end_byte());
2149
2150    let mut cursor = node.walk();
2151    for child in node.children(&mut cursor) {
2152        if child.kind() == ","
2153            && child.start_byte() >= name_node.end_byte()
2154            && next_name_start.map_or(true, |next| child.start_byte() < next)
2155        {
2156            segment_end = child.start_byte();
2157            break;
2158        }
2159    }
2160
2161    segment_end
2162}
2163
2164/// Rust: `let x: Type = ...` or `let x = Foo::new()`
2165fn scan_rust_let_declaration(
2166    node: tree_sitter::Node,
2167    scope_idx: usize,
2168    scopes: &mut Vec<Scope>,
2169    source: &[u8],
2170) {
2171    let var_name = node
2172        .child_by_field_name("pattern")
2173        .and_then(|n| {
2174            // Pattern can be just an identifier or `mut x`
2175            if n.kind() == "identifier" {
2176                n.utf8_text(source).ok()
2177            } else if n.kind() == "mut_pattern" {
2178                n.named_child(0).and_then(|c| c.utf8_text(source).ok())
2179            } else {
2180                None
2181            }
2182        })
2183        .unwrap_or("")
2184        .to_string();
2185
2186    if var_name.is_empty() {
2187        return;
2188    }
2189    record_binding(scopes, scope_idx, &var_name, node.start_position().row);
2190
2191    // Check for explicit type annotation: `let x: Connection = ...`
2192    if let Some(type_node) = node.child_by_field_name("type") {
2193        let type_text = extract_base_type(type_node, source);
2194        if !type_text.is_empty() && type_text.chars().next().map_or(false, |c| c.is_uppercase()) {
2195            scopes[scope_idx].types.insert(var_name, type_text);
2196            return;
2197        }
2198    }
2199
2200    // Check RHS value
2201    if let Some(value) = node.child_by_field_name("value") {
2202        record_type_from_rhs(value, &var_name, scope_idx, scopes, source);
2203    }
2204}
2205
2206/// Go: `x := Foo{}` or `x := NewFoo()`
2207fn scan_go_short_var(
2208    node: tree_sitter::Node,
2209    scope_idx: usize,
2210    scopes: &mut Vec<Scope>,
2211    source: &[u8],
2212) {
2213    let left = match node.child_by_field_name("left") {
2214        Some(l) => l,
2215        None => return,
2216    };
2217    let right = match node.child_by_field_name("right") {
2218        Some(r) => r,
2219        None => return,
2220    };
2221
2222    // left is expression_list, right is expression_list
2223    let var_name = if left.kind() == "expression_list" {
2224        left.named_child(0)
2225            .and_then(|n| n.utf8_text(source).ok())
2226            .unwrap_or("")
2227            .to_string()
2228    } else {
2229        left.utf8_text(source).unwrap_or("").to_string()
2230    };
2231
2232    if var_name.is_empty() {
2233        return;
2234    }
2235    record_binding(scopes, scope_idx, &var_name, left.start_position().row);
2236
2237    let rhs = if right.kind() == "expression_list" {
2238        match right.named_child(0) {
2239            Some(n) => n,
2240            None => return,
2241        }
2242    } else {
2243        right
2244    };
2245
2246    record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
2247}
2248
2249/// Go: `var x Type = ...` or `var x = Foo{}`
2250fn scan_go_var_declaration(
2251    node: tree_sitter::Node,
2252    scope_idx: usize,
2253    scopes: &mut Vec<Scope>,
2254    source: &[u8],
2255) {
2256    let mut cursor = node.walk();
2257    for child in node.named_children(&mut cursor) {
2258        if child.kind() == "var_spec" {
2259            let var_name = child
2260                .child_by_field_name("name")
2261                .and_then(|n| n.utf8_text(source).ok())
2262                .unwrap_or("")
2263                .to_string();
2264            if var_name.is_empty() {
2265                // Try first named child as name
2266                if let Some(first) = child.named_child(0) {
2267                    if first.kind() == "identifier" {
2268                        let name = first.utf8_text(source).unwrap_or("").to_string();
2269                        if !name.is_empty() {
2270                            record_binding(scopes, scope_idx, &name, first.start_position().row);
2271                            // Check for type child
2272                            if let Some(type_node) = child.child_by_field_name("type") {
2273                                let type_text = extract_base_type(type_node, source);
2274                                if !type_text.is_empty()
2275                                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
2276                                {
2277                                    scopes[scope_idx].types.insert(name, type_text);
2278                                }
2279                            }
2280                        }
2281                    }
2282                }
2283                continue;
2284            }
2285            let binding_row = child
2286                .child_by_field_name("name")
2287                .map(|n| n.start_position().row)
2288                .unwrap_or_else(|| child.start_position().row);
2289            record_binding(scopes, scope_idx, &var_name, binding_row);
2290
2291            // Check for explicit type
2292            if let Some(type_node) = child.child_by_field_name("type") {
2293                let type_text = extract_base_type(type_node, source);
2294                if !type_text.is_empty()
2295                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
2296                {
2297                    scopes[scope_idx].types.insert(var_name, type_text);
2298                    continue;
2299                }
2300            }
2301
2302            // Check RHS value
2303            if let Some(value) = child.child_by_field_name("value") {
2304                let rhs = if value.kind() == "expression_list" {
2305                    value.named_child(0).unwrap_or(value)
2306                } else {
2307                    value
2308                };
2309                record_type_from_rhs(rhs, &var_name, scope_idx, scopes, source);
2310            }
2311        }
2312    }
2313}
2314
2315/// Record type binding from a RHS expression (works for all languages).
2316/// Handles: constructor calls, new expressions, struct literals, function calls.
2317fn record_type_from_rhs(
2318    rhs: tree_sitter::Node,
2319    var_name: &str,
2320    scope_idx: usize,
2321    scopes: &mut Vec<Scope>,
2322    source: &[u8],
2323) {
2324    match rhs.kind() {
2325        // Python/Go: Foo() or func()
2326        "call" | "call_expression" => {
2327            let func_node = rhs
2328                .child_by_field_name("function")
2329                .or_else(|| rhs.named_child(0));
2330            if let Some(func) = func_node {
2331                if func.kind() == "identifier"
2332                    || func.kind() == "simple_identifier"
2333                    || func.kind() == "type_identifier"
2334                {
2335                    let name = func.utf8_text(source).unwrap_or("");
2336                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
2337                        scopes[scope_idx]
2338                            .types
2339                            .insert(var_name.to_string(), name.to_string());
2340                    } else {
2341                        scopes[scope_idx]
2342                            .pending_call_types
2343                            .insert(var_name.to_string(), name.to_string());
2344                    }
2345                }
2346                // Rust: Type::new() / Type::from() etc.
2347                if func.kind() == "scoped_identifier" {
2348                    let text = func.utf8_text(source).unwrap_or("");
2349                    let parts: Vec<&str> = text.split("::").collect();
2350                    if parts.len() >= 2 {
2351                        let type_name = parts[0];
2352                        let method_name = parts[parts.len() - 1];
2353                        if type_name.chars().next().map_or(false, |c| c.is_uppercase()) {
2354                            scopes[scope_idx]
2355                                .types
2356                                .insert(var_name.to_string(), type_name.to_string());
2357                        } else {
2358                            scopes[scope_idx]
2359                                .pending_call_types
2360                                .insert(var_name.to_string(), method_name.to_string());
2361                        }
2362                    }
2363                }
2364                // Go: package.NewFoo() or package.GetFoo()
2365                if func.kind() == "selector_expression" {
2366                    let field = func
2367                        .child_by_field_name("field")
2368                        .and_then(|n| n.utf8_text(source).ok())
2369                        .unwrap_or("");
2370                    // Go convention: NewFoo() returns *Foo
2371                    if let Some(type_name) = field.strip_prefix("New") {
2372                        if !type_name.is_empty()
2373                            && type_name.chars().next().map_or(false, |c| c.is_uppercase())
2374                        {
2375                            scopes[scope_idx]
2376                                .types
2377                                .insert(var_name.to_string(), type_name.to_string());
2378                        }
2379                    } else if field.starts_with("Get")
2380                        || field.chars().next().map_or(false, |c| c.is_uppercase())
2381                    {
2382                        // Other Go package functions: record for return type resolution
2383                        scopes[scope_idx]
2384                            .pending_call_types
2385                            .insert(var_name.to_string(), field.to_string());
2386                    }
2387                }
2388            }
2389        }
2390        // TS: new Foo()
2391        "new_expression" => {
2392            if let Some(constructor) = rhs.child_by_field_name("constructor") {
2393                let name = constructor.utf8_text(source).unwrap_or("");
2394                if !name.is_empty() {
2395                    scopes[scope_idx]
2396                        .types
2397                        .insert(var_name.to_string(), name.to_string());
2398                }
2399            }
2400        }
2401        // Go: Foo{} (composite_literal / struct literal)
2402        "composite_literal" => {
2403            if let Some(type_node) = rhs.child_by_field_name("type") {
2404                let name = type_node.utf8_text(source).unwrap_or("");
2405                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
2406                    scopes[scope_idx]
2407                        .types
2408                        .insert(var_name.to_string(), name.to_string());
2409                }
2410            }
2411        }
2412        _ => {}
2413    }
2414}
2415
2416/// Extract the base type name from a type annotation node.
2417/// Strips pointers, references, generics to get just the type name.
2418fn extract_base_type(type_node: tree_sitter::Node, source: &[u8]) -> String {
2419    let text = type_node.utf8_text(source).unwrap_or("").trim().to_string();
2420    // Strip reference/pointer prefixes and mut keyword
2421    let text = text.trim_start_matches('&').trim_start_matches('*');
2422    let text = text.strip_prefix("mut ").unwrap_or(text).trim_start();
2423    // Strip generic parameters (angle brackets and Python-style square brackets)
2424    let text = if let Some(i) = text.find('<') {
2425        &text[..i]
2426    } else if let Some(i) = text.find('[') {
2427        &text[..i]
2428    } else {
2429        text
2430    };
2431    // Strip lifetime annotations for Rust
2432    let text = text.trim();
2433    // For type_annotation nodes in TS, strip the leading `: `
2434    let text = text.trim_start_matches(':').trim();
2435    text.to_string()
2436}
2437
2438/// Parse Go receiver type from method content: `func (r *ReceiverType) Name(...)`
2439pub fn extract_go_receiver_type(content: &str) -> Option<String> {
2440    let after_func = content.strip_prefix("func")?.trim_start();
2441    let paren_start = after_func.find('(')?;
2442    let paren_end = after_func.find(')')?;
2443    let receiver_block = &after_func[paren_start + 1..paren_end];
2444    // Could be: "r ReceiverType", "r *ReceiverType", "*ReceiverType"
2445    let parts: Vec<&str> = receiver_block.split_whitespace().collect();
2446    let type_str = parts.last()?;
2447    let name = type_str.trim_start_matches('*');
2448    if name.is_empty() {
2449        None
2450    } else {
2451        Some(name.to_string())
2452    }
2453}
2454
2455/// Build Go package index: pkg_name → [(entity_name, entity_id)]
2456/// Maps file stems and parent directory names to entities for O(1) package import lookup.
2457pub(crate) fn build_go_pkg_index(
2458    symbol_table: &HashMap<String, Vec<String>>,
2459    entity_map: &HashMap<String, EntityInfo>,
2460) -> HashMap<String, Vec<(String, String)>> {
2461    let mut idx: HashMap<String, Vec<(String, String)>> = HashMap::new();
2462    for (name, target_ids) in symbol_table.iter() {
2463        for target_id in target_ids {
2464            if let Some(entity) = entity_map.get(target_id) {
2465                if !entity.file_path.ends_with(".go") {
2466                    continue;
2467                }
2468                let file_stem = entity
2469                    .file_path
2470                    .rsplit('/')
2471                    .next()
2472                    .unwrap_or(&entity.file_path);
2473                let file_stem = file_stem.strip_suffix(".go").unwrap_or(file_stem);
2474                idx.entry(file_stem.to_string())
2475                    .or_default()
2476                    .push((name.clone(), target_id.clone()));
2477                if let Some(parent_start) = entity.file_path.rfind('/') {
2478                    let parent_path = &entity.file_path[..parent_start];
2479                    if let Some(dir_name_start) = parent_path.rfind('/') {
2480                        let dir_name = &parent_path[dir_name_start + 1..];
2481                        if dir_name != file_stem {
2482                            idx.entry(dir_name.to_string())
2483                                .or_default()
2484                                .push((name.clone(), target_id.clone()));
2485                        }
2486                    } else if !parent_path.is_empty() && parent_path != file_stem {
2487                        idx.entry(parent_path.to_string())
2488                            .or_default()
2489                            .push((name.clone(), target_id.clone()));
2490                    }
2491                }
2492            }
2493        }
2494    }
2495    for entries in idx.values_mut() {
2496        entries.sort_unstable();
2497    }
2498    idx
2499}
2500
2501/// Scan function bodies/signatures for return types to build a return type map.
2502fn scan_return_types(
2503    root: tree_sitter::Node,
2504    _file_path: &str,
2505    file_lookup: &FileEntityLookup<'_>,
2506    source: &[u8],
2507    return_type_map: &mut HashMap<String, String>,
2508    config: &ScopeResolveConfig,
2509) {
2510    let mut worklist = vec![root];
2511    while let Some(node) = worklist.pop() {
2512        let kind = node.kind();
2513
2514        let is_func = config.function_scope_nodes.contains(&kind);
2515
2516        if is_func {
2517            let func_name = node
2518                .child_by_field_name("name")
2519                .and_then(|n| n.utf8_text(source).ok())
2520                .unwrap_or("");
2521
2522            let line = node.start_position().row + 1;
2523            let func_entity = file_lookup.find_at_line(func_name, line, |_| true);
2524
2525            if let Some(fe) = func_entity {
2526                // Try explicit return type annotation first
2527                let ret_type = config.return_type_field.and_then(|field| {
2528                    node.child_by_field_name(field)
2529                        .map(|n| extract_base_type(n, source))
2530                        .filter(|t| {
2531                            !t.is_empty() && t.chars().next().map_or(false, |c| c.is_uppercase())
2532                        })
2533                });
2534
2535                if let Some(rt) = ret_type {
2536                    return_type_map.insert(fe.id.clone(), rt);
2537                } else {
2538                    // Fall back to body heuristic: return ClassName()
2539                    if let Some(ret_type) = find_return_constructor(node, source) {
2540                        return_type_map.insert(fe.id.clone(), ret_type);
2541                    }
2542                }
2543            }
2544        }
2545
2546        push_named_children_rev(&mut worklist, node);
2547    }
2548}
2549
2550/// Find `return ClassName()` patterns in a function body (heuristic fallback).
2551fn find_return_constructor(root: tree_sitter::Node, source: &[u8]) -> Option<String> {
2552    let mut worklist = vec![root];
2553    while let Some(node) = worklist.pop() {
2554        let mut cursor = node.walk();
2555        for child in node.named_children(&mut cursor) {
2556            if child.kind() == "return_statement" {
2557                let mut inner_cursor = child.walk();
2558                for ret_child in child.named_children(&mut inner_cursor) {
2559                    // Python: call, TS/Go: call_expression
2560                    if ret_child.kind() == "call" || ret_child.kind() == "call_expression" {
2561                        if let Some(func) = ret_child.child_by_field_name("function") {
2562                            if func.kind() == "identifier" {
2563                                let name = func.utf8_text(source).unwrap_or("");
2564                                if name.chars().next().map_or(false, |c| c.is_uppercase()) {
2565                                    return Some(name.to_string());
2566                                }
2567                            }
2568                        }
2569                    }
2570                    // TS: new ClassName()
2571                    if ret_child.kind() == "new_expression" {
2572                        if let Some(constructor) = ret_child.child_by_field_name("constructor") {
2573                            let name = constructor.utf8_text(source).unwrap_or("");
2574                            if !name.is_empty() {
2575                                return Some(name.to_string());
2576                            }
2577                        }
2578                    }
2579                    // Go: StructName{} (composite_literal)
2580                    if ret_child.kind() == "composite_literal" {
2581                        if let Some(type_node) = ret_child.child_by_field_name("type") {
2582                            let name = type_node.utf8_text(source).unwrap_or("");
2583                            if name.chars().next().map_or(false, |c| c.is_uppercase()) {
2584                                return Some(name.to_string());
2585                            }
2586                        }
2587                    }
2588                }
2589            }
2590            // Recurse into blocks
2591            let ck = child.kind();
2592            if ck == "block" || ck == "statement_block" {
2593                worklist.push(child);
2594            }
2595        }
2596    }
2597    None
2598}
2599
2600/// Scan for instance attribute types: __init__ self.attr patterns (Python/TS),
2601/// struct field declarations (Rust/Go).
2602fn scan_init_self_attrs(
2603    root: tree_sitter::Node,
2604    _file_path: &str,
2605    _file_entities: &[&SemanticEntity],
2606    _entity_map: &HashMap<String, EntityInfo>,
2607    source: &[u8],
2608    instance_attr_types: &mut HashMap<(String, String), String>,
2609    init_params_map: &mut HashMap<String, Vec<String>>,
2610    attr_to_param_map: &mut HashMap<(String, String), String>,
2611    config: &ScopeResolveConfig,
2612) {
2613    let mut worklist = vec![root];
2614    while let Some(node) = worklist.pop() {
2615        let kind = node.kind();
2616
2617        match &config.init_strategy {
2618            InitStrategy::ConstructorBody {
2619                class_nodes,
2620                init_node_kind,
2621                self_keyword: _,
2622                ..
2623            } => {
2624                if class_nodes.contains(&kind) {
2625                    let class_name = node
2626                        .child_by_field_name("name")
2627                        .and_then(|n| n.utf8_text(source).ok())
2628                        .unwrap_or("")
2629                        .to_string();
2630
2631                    if !class_name.is_empty() {
2632                        // Determine lang for scan_class_for_init using init_node_kind as discriminator
2633                        let lang = match *init_node_kind {
2634                            "function_definition" => "python",
2635                            "method_definition" => "typescript",
2636                            "init_declaration" => "swift",
2637                            "anonymous_initializer" => "kotlin",
2638                            _ => "typescript",
2639                        };
2640                        scan_class_for_init(
2641                            node,
2642                            &class_name,
2643                            source,
2644                            instance_attr_types,
2645                            init_params_map,
2646                            attr_to_param_map,
2647                            lang,
2648                        );
2649                    }
2650                }
2651            }
2652            InitStrategy::StructFields { struct_nodes } => {
2653                if struct_nodes.contains(&kind) {
2654                    // Rust struct: extract field types directly
2655                    if kind == "struct_item" {
2656                        let struct_name = node
2657                            .child_by_field_name("name")
2658                            .and_then(|n| n.utf8_text(source).ok())
2659                            .unwrap_or("")
2660                            .to_string();
2661
2662                        if !struct_name.is_empty() {
2663                            scan_rust_struct_fields(
2664                                node,
2665                                &struct_name,
2666                                source,
2667                                instance_attr_types,
2668                            );
2669                        }
2670                    }
2671                    // Go: extract field types from type declarations
2672                    if kind == "type_declaration" {
2673                        scan_go_struct_fields(node, source, instance_attr_types);
2674                    }
2675                }
2676            }
2677            InitStrategy::None => {}
2678        }
2679
2680        push_named_children_rev(&mut worklist, node);
2681    }
2682}
2683
2684/// Rust: extract field types from `struct Foo { conn: Connection, ... }`
2685fn scan_rust_struct_fields(
2686    node: tree_sitter::Node,
2687    struct_name: &str,
2688    source: &[u8],
2689    instance_attr_types: &mut HashMap<(String, String), String>,
2690) {
2691    let mut cursor = node.walk();
2692    for child in node.named_children(&mut cursor) {
2693        if child.kind() == "field_declaration_list" {
2694            let mut inner_cursor = child.walk();
2695            for field in child.named_children(&mut inner_cursor) {
2696                if field.kind() == "field_declaration" {
2697                    let field_name = field
2698                        .child_by_field_name("name")
2699                        .and_then(|n| n.utf8_text(source).ok())
2700                        .unwrap_or("");
2701                    let field_type = field
2702                        .child_by_field_name("type")
2703                        .map(|n| extract_base_type(n, source))
2704                        .unwrap_or_default();
2705
2706                    if !field_name.is_empty()
2707                        && !field_type.is_empty()
2708                        && field_type
2709                            .chars()
2710                            .next()
2711                            .map_or(false, |c| c.is_uppercase())
2712                    {
2713                        instance_attr_types.insert(
2714                            (struct_name.to_string(), field_name.to_string()),
2715                            field_type,
2716                        );
2717                    }
2718                }
2719            }
2720        }
2721    }
2722}
2723
2724/// Go: extract field types from `type Foo struct { conn Connection; ... }`
2725fn scan_go_struct_fields(
2726    node: tree_sitter::Node,
2727    source: &[u8],
2728    instance_attr_types: &mut HashMap<(String, String), String>,
2729) {
2730    let mut cursor = node.walk();
2731    for child in node.named_children(&mut cursor) {
2732        if child.kind() == "type_spec" {
2733            let struct_name = child
2734                .child_by_field_name("name")
2735                .and_then(|n| n.utf8_text(source).ok())
2736                .unwrap_or("")
2737                .to_string();
2738
2739            if struct_name.is_empty() {
2740                continue;
2741            }
2742
2743            // Look for struct_type child
2744            if let Some(type_node) = child.child_by_field_name("type") {
2745                if type_node.kind() == "struct_type" {
2746                    let mut fields_cursor = type_node.walk();
2747                    for field_list in type_node.named_children(&mut fields_cursor) {
2748                        if field_list.kind() == "field_declaration_list" {
2749                            let mut inner = field_list.walk();
2750                            for field in field_list.named_children(&mut inner) {
2751                                if field.kind() == "field_declaration" {
2752                                    // Go field: name type
2753                                    let field_name = field
2754                                        .child_by_field_name("name")
2755                                        .and_then(|n| n.utf8_text(source).ok())
2756                                        .unwrap_or("");
2757                                    let field_type = field
2758                                        .child_by_field_name("type")
2759                                        .map(|n| extract_base_type(n, source))
2760                                        .unwrap_or_default();
2761
2762                                    if !field_name.is_empty()
2763                                        && !field_type.is_empty()
2764                                        && field_type
2765                                            .chars()
2766                                            .next()
2767                                            .map_or(false, |c| c.is_uppercase())
2768                                    {
2769                                        instance_attr_types.insert(
2770                                            (struct_name.clone(), field_name.to_string()),
2771                                            field_type,
2772                                        );
2773                                    }
2774                                }
2775                            }
2776                        }
2777                    }
2778                }
2779            }
2780        }
2781    }
2782}
2783
2784fn scan_class_for_init(
2785    root: tree_sitter::Node,
2786    class_name: &str,
2787    source: &[u8],
2788    instance_attr_types: &mut HashMap<(String, String), String>,
2789    init_params_map: &mut HashMap<String, Vec<String>>,
2790    attr_to_param_map: &mut HashMap<(String, String), String>,
2791    lang: &str,
2792) {
2793    // Kotlin: extract primary constructor params (class_parameter nodes with val/var)
2794    if lang == "kotlin" {
2795        scan_kotlin_primary_constructor(root, class_name, source, instance_attr_types);
2796    }
2797
2798    let mut worklist = vec![root];
2799    while let Some(node) = worklist.pop() {
2800        let mut cursor = node.walk();
2801        for child in node.named_children(&mut cursor) {
2802            let ck = child.kind();
2803
2804            // Python __init__
2805            if ck == "function_definition" && lang == "python" {
2806                let name = child
2807                    .child_by_field_name("name")
2808                    .and_then(|n| n.utf8_text(source).ok())
2809                    .unwrap_or("");
2810                if name == "__init__" {
2811                    let params = extract_init_params(child, source);
2812                    let ordered_params = extract_init_param_names_ordered(child, source);
2813                    init_params_map.insert(class_name.to_string(), ordered_params);
2814                    scan_init_body(
2815                        child,
2816                        class_name,
2817                        &params,
2818                        source,
2819                        instance_attr_types,
2820                        attr_to_param_map,
2821                    );
2822                }
2823            }
2824
2825            // TS constructor
2826            if ck == "method_definition" && lang == "typescript" {
2827                let name = child
2828                    .child_by_field_name("name")
2829                    .and_then(|n| n.utf8_text(source).ok())
2830                    .unwrap_or("");
2831                if name == "constructor" {
2832                    // Scan for this.attr = param patterns
2833                    scan_ts_constructor_body(
2834                        child,
2835                        class_name,
2836                        source,
2837                        instance_attr_types,
2838                        init_params_map,
2839                        attr_to_param_map,
2840                    );
2841                }
2842            }
2843
2844            // Swift init_declaration
2845            if ck == "init_declaration" && lang == "swift" {
2846                scan_swift_init_body(
2847                    child,
2848                    class_name,
2849                    source,
2850                    instance_attr_types,
2851                    init_params_map,
2852                    attr_to_param_map,
2853                );
2854            }
2855
2856            // Kotlin anonymous_initializer (init { ... } block)
2857            if ck == "anonymous_initializer" && lang == "kotlin" {
2858                scan_kotlin_init_body(
2859                    child,
2860                    class_name,
2861                    source,
2862                    instance_attr_types,
2863                    attr_to_param_map,
2864                );
2865            }
2866
2867            // TS: typed class field declarations `private conn: Connection`
2868            if (ck == "public_field_definition"
2869                || ck == "property_declaration"
2870                || ck == "field_definition")
2871                && lang == "typescript"
2872            {
2873                let field_name = child
2874                    .child_by_field_name("name")
2875                    .and_then(|n| n.utf8_text(source).ok())
2876                    .unwrap_or("");
2877                if let Some(type_ann) = child.child_by_field_name("type") {
2878                    let type_text = extract_base_type(type_ann, source);
2879                    if !field_name.is_empty()
2880                        && !type_text.is_empty()
2881                        && type_text.chars().next().map_or(false, |c| c.is_uppercase())
2882                    {
2883                        instance_attr_types
2884                            .insert((class_name.to_string(), field_name.to_string()), type_text);
2885                    }
2886                }
2887            }
2888
2889            // Swift: typed property declarations `var conn: Connection`
2890            if ck == "property_declaration" && lang == "swift" {
2891                scan_swift_property_declaration(child, class_name, source, instance_attr_types);
2892            }
2893
2894            // Kotlin: typed property declarations `val conn: Connection`
2895            if ck == "property_declaration" && lang == "kotlin" {
2896                scan_kotlin_property_declaration(child, class_name, source, instance_attr_types);
2897            }
2898
2899            if ck == "block"
2900                || ck == "class_body"
2901                || ck == "statement_block"
2902                || ck == "struct_body"
2903                || ck == "function_body"
2904                || ck == "code_block"
2905                || ck == "statements"
2906                || ck == "enum_class_body"
2907            {
2908                worklist.push(child);
2909            }
2910        }
2911    }
2912}
2913
2914/// Swift: scan init body for `self.attr = param` patterns
2915fn scan_swift_init_body(
2916    node: tree_sitter::Node,
2917    class_name: &str,
2918    source: &[u8],
2919    instance_attr_types: &mut HashMap<(String, String), String>,
2920    init_params_map: &mut HashMap<String, Vec<String>>,
2921    attr_to_param_map: &mut HashMap<(String, String), String>,
2922) {
2923    let params = extract_init_params(node, source);
2924    let ordered_params = extract_init_param_names_ordered(node, source);
2925    init_params_map.insert(class_name.to_string(), ordered_params);
2926
2927    // Walk body looking for self.X = Y
2928    let mut worklist = vec![node];
2929    while let Some(wnode) = worklist.pop() {
2930        let mut cursor = wnode.walk();
2931        for child in wnode.named_children(&mut cursor) {
2932            let ck = child.kind();
2933            // Look for assignment: self.X = Y via directly_assigned_expression or assignment
2934            if ck == "directly_assigned_expression" || ck == "assignment" {
2935                if let Some(left) = child
2936                    .child_by_field_name("left")
2937                    .or_else(|| child.named_child(0))
2938                {
2939                    if left.kind() == "navigation_expression" {
2940                        let obj = left
2941                            .child_by_field_name("target")
2942                            .and_then(|n| n.utf8_text(source).ok())
2943                            .unwrap_or("");
2944                        let prop = left
2945                            .child_by_field_name("suffix")
2946                            .and_then(|n| n.utf8_text(source).ok())
2947                            .map(|text| text.strip_prefix('.').unwrap_or(text))
2948                            .unwrap_or("");
2949                        if obj == "self" && !prop.is_empty() {
2950                            if let Some(right) = child
2951                                .child_by_field_name("right")
2952                                .or_else(|| child.named_child(1))
2953                            {
2954                                if right.kind() == "simple_identifier"
2955                                    || right.kind() == "identifier"
2956                                {
2957                                    let rhs_name = right.utf8_text(source).unwrap_or("");
2958                                    if params.contains_key(rhs_name) {
2959                                        attr_to_param_map.insert(
2960                                            (class_name.to_string(), prop.to_string()),
2961                                            rhs_name.to_string(),
2962                                        );
2963                                        if let Some(Some(type_hint)) = params.get(rhs_name) {
2964                                            instance_attr_types.insert(
2965                                                (class_name.to_string(), prop.to_string()),
2966                                                type_hint.clone(),
2967                                            );
2968                                        }
2969                                    }
2970                                }
2971                            }
2972                        }
2973                    }
2974                }
2975            }
2976            if ck == "function_body"
2977                || ck == "code_block"
2978                || ck == "statements"
2979                || ck == "expression_statement"
2980                || ck == "block"
2981            {
2982                worklist.push(child);
2983            }
2984        }
2985    }
2986}
2987
2988/// Swift: extract typed property declarations `var conn: Connection`
2989fn scan_swift_property_declaration(
2990    node: tree_sitter::Node,
2991    class_name: &str,
2992    source: &[u8],
2993    instance_attr_types: &mut HashMap<(String, String), String>,
2994) {
2995    let mut processed_pattern_binding = false;
2996
2997    let mut cursor = node.walk();
2998
2999    for child in node.named_children(&mut cursor) {
3000        if child.kind() == "pattern_binding" {
3001            processed_pattern_binding = true;
3002            scan_swift_property_binding(child, class_name, source, instance_attr_types);
3003        }
3004    }
3005    if processed_pattern_binding {
3006        return;
3007    }
3008
3009    // Swift property_declaration nodes vary by grammar version. Some expose
3010    // pattern/type_annotation pairs directly instead of pattern_binding nodes.
3011    let mut pending_names = Vec::new();
3012    let mut cursor = node.walk();
3013    for child in node.named_children(&mut cursor) {
3014        match child.kind() {
3015            "pattern" | "simple_identifier" | "identifier" => {
3016                if let Some(name) = extract_swift_property_pattern_name(child, source) {
3017                    pending_names.push(name);
3018                }
3019            }
3020            "type_annotation" | "user_type" | "type_identifier" => {
3021                let type_text = extract_base_type(child, source);
3022                if !type_text.is_empty()
3023                    && type_text.chars().next().map_or(false, |c| c.is_uppercase())
3024                {
3025                    for name in pending_names.drain(..) {
3026                        instance_attr_types
3027                            .insert((class_name.to_string(), name), type_text.clone());
3028                    }
3029                }
3030            }
3031            "call_expression" | "new_expression" | "value_argument" => pending_names.clear(),
3032            _ => {}
3033        }
3034    }
3035}
3036
3037fn scan_swift_property_binding(
3038    node: tree_sitter::Node,
3039    class_name: &str,
3040    source: &[u8],
3041    instance_attr_types: &mut HashMap<(String, String), String>,
3042) {
3043    let mut field_names = Vec::new();
3044    let mut field_type = node.child_by_field_name("type").and_then(|type_node| {
3045        let type_text = extract_base_type(type_node, source);
3046        if type_text.is_empty() {
3047            None
3048        } else {
3049            Some(type_text)
3050        }
3051    });
3052
3053    let mut cursor = node.walk();
3054    for child in node.named_children(&mut cursor) {
3055        match child.kind() {
3056            "pattern" | "simple_identifier" | "identifier" => {
3057                if let Some(name) = extract_swift_property_pattern_name(child, source) {
3058                    field_names.push(name);
3059                }
3060            }
3061            "type_annotation" => {
3062                if field_type.is_none() {
3063                    let type_text = extract_base_type(child, source);
3064                    if !type_text.is_empty() {
3065                        field_type = Some(type_text);
3066                    }
3067                }
3068            }
3069            _ => {}
3070        }
3071    }
3072
3073    let Some(type_text) = field_type else {
3074        return;
3075    };
3076    if !type_text.chars().next().map_or(false, |c| c.is_uppercase()) {
3077        return;
3078    }
3079
3080    for field_name in field_names {
3081        instance_attr_types.insert((class_name.to_string(), field_name), type_text.clone());
3082    }
3083}
3084
3085fn extract_swift_property_pattern_name(node: tree_sitter::Node, source: &[u8]) -> Option<String> {
3086    if matches!(node.kind(), "simple_identifier" | "identifier") {
3087        let name = node.utf8_text(source).ok()?.trim();
3088        return (!name.is_empty()).then(|| name.to_string());
3089    }
3090
3091    if let Some(name_node) = node.child_by_field_name("name") {
3092        return extract_swift_property_pattern_name(name_node, source);
3093    }
3094
3095    let mut cursor = node.walk();
3096    for child in node.named_children(&mut cursor) {
3097        if matches!(child.kind(), "simple_identifier" | "identifier") {
3098            return extract_swift_property_pattern_name(child, source);
3099        }
3100    }
3101
3102    None
3103}
3104
3105/// Kotlin: extract typed property declarations `val conn: Connection`
3106fn scan_kotlin_property_declaration(
3107    node: tree_sitter::Node,
3108    class_name: &str,
3109    source: &[u8],
3110    instance_attr_types: &mut HashMap<(String, String), String>,
3111) {
3112    let field_name = node
3113        .child_by_field_name("name")
3114        .and_then(|n| n.utf8_text(source).ok())
3115        .unwrap_or("");
3116    let field_type = node
3117        .child_by_field_name("type")
3118        .map(|n| extract_base_type(n, source))
3119        .unwrap_or_default();
3120
3121    if !field_name.is_empty()
3122        && !field_type.is_empty()
3123        && field_type
3124            .chars()
3125            .next()
3126            .map_or(false, |c| c.is_uppercase())
3127    {
3128        instance_attr_types.insert((class_name.to_string(), field_name.to_string()), field_type);
3129    }
3130}
3131
3132/// Kotlin: extract primary constructor params with val/var as instance attributes
3133fn scan_kotlin_primary_constructor(
3134    class_node: tree_sitter::Node,
3135    class_name: &str,
3136    source: &[u8],
3137    instance_attr_types: &mut HashMap<(String, String), String>,
3138) {
3139    // Look for primary_constructor child, then class_parameter nodes
3140    let mut cursor = class_node.walk();
3141    for child in class_node.named_children(&mut cursor) {
3142        if child.kind() == "primary_constructor" {
3143            let mut pc_cursor = child.walk();
3144            for param in child.named_children(&mut pc_cursor) {
3145                if param.kind() == "class_parameter" {
3146                    // Check if this has val/var modifier (makes it a property)
3147                    let text = param.utf8_text(source).unwrap_or("");
3148                    let has_val_var = text.starts_with("val ")
3149                        || text.starts_with("var ")
3150                        || text.contains("val ")
3151                        || text.contains("var ");
3152                    if has_val_var {
3153                        let param_name = param
3154                            .child_by_field_name("name")
3155                            .and_then(|n| n.utf8_text(source).ok())
3156                            .unwrap_or("");
3157                        let param_type = param
3158                            .child_by_field_name("type")
3159                            .map(|n| extract_base_type(n, source))
3160                            .unwrap_or_default();
3161                        if !param_name.is_empty()
3162                            && !param_type.is_empty()
3163                            && param_type
3164                                .chars()
3165                                .next()
3166                                .map_or(false, |c| c.is_uppercase())
3167                        {
3168                            instance_attr_types.insert(
3169                                (class_name.to_string(), param_name.to_string()),
3170                                param_type,
3171                            );
3172                        }
3173                    }
3174                }
3175            }
3176        }
3177    }
3178}
3179
3180/// Kotlin: scan init { ... } body for this.attr = expr patterns
3181fn scan_kotlin_init_body(
3182    node: tree_sitter::Node,
3183    class_name: &str,
3184    source: &[u8],
3185    instance_attr_types: &mut HashMap<(String, String), String>,
3186    attr_to_param_map: &mut HashMap<(String, String), String>,
3187) {
3188    let mut worklist = vec![node];
3189    while let Some(wnode) = worklist.pop() {
3190        let mut cursor = wnode.walk();
3191        for child in wnode.named_children(&mut cursor) {
3192            let ck = child.kind();
3193            if ck == "assignment" || ck == "directly_assigned_expression" {
3194                if let Some(left) = child
3195                    .child_by_field_name("left")
3196                    .or_else(|| child.named_child(0))
3197                {
3198                    if left.kind() == "navigation_expression" {
3199                        let obj = left
3200                            .child_by_field_name("expression")
3201                            .and_then(|n| n.utf8_text(source).ok())
3202                            .unwrap_or("");
3203                        let prop = left
3204                            .child_by_field_name("navigation_suffix")
3205                            .and_then(|n| n.utf8_text(source).ok())
3206                            .unwrap_or("");
3207                        if obj == "this" && !prop.is_empty() {
3208                            if let Some(right) = child
3209                                .child_by_field_name("right")
3210                                .or_else(|| child.named_child(1))
3211                            {
3212                                if right.kind() == "simple_identifier"
3213                                    || right.kind() == "identifier"
3214                                {
3215                                    let rhs_name = right.utf8_text(source).unwrap_or("");
3216                                    attr_to_param_map.insert(
3217                                        (class_name.to_string(), prop.to_string()),
3218                                        rhs_name.to_string(),
3219                                    );
3220                                }
3221                                // If RHS is a constructor call, record type directly
3222                                if right.kind() == "call_expression" {
3223                                    let callee = right
3224                                        .child_by_field_name("function")
3225                                        .and_then(|n| n.utf8_text(source).ok())
3226                                        .unwrap_or("");
3227                                    if !callee.is_empty()
3228                                        && callee.chars().next().map_or(false, |c| c.is_uppercase())
3229                                    {
3230                                        instance_attr_types.insert(
3231                                            (class_name.to_string(), prop.to_string()),
3232                                            callee.to_string(),
3233                                        );
3234                                    }
3235                                }
3236                            }
3237                        }
3238                    }
3239                }
3240            }
3241            if ck == "statements" || ck == "block" || ck == "expression_statement" {
3242                worklist.push(child);
3243            }
3244        }
3245    }
3246}
3247
3248/// TS: scan constructor body for `this.attr = param` patterns
3249fn scan_ts_constructor_body(
3250    node: tree_sitter::Node,
3251    class_name: &str,
3252    source: &[u8],
3253    instance_attr_types: &mut HashMap<(String, String), String>,
3254    init_params_map: &mut HashMap<String, Vec<String>>,
3255    attr_to_param_map: &mut HashMap<(String, String), String>,
3256) {
3257    // Extract constructor params
3258    let params = extract_init_params(node, source);
3259    let ordered_params = extract_init_param_names_ordered(node, source);
3260    init_params_map.insert(class_name.to_string(), ordered_params);
3261
3262    // Scan body for this.X = param
3263    scan_init_body_this(
3264        node,
3265        class_name,
3266        &params,
3267        source,
3268        instance_attr_types,
3269        attr_to_param_map,
3270    );
3271}
3272
3273/// Scan constructor body for `this.attr = param` patterns (TS variant)
3274fn scan_init_body_this(
3275    root: tree_sitter::Node,
3276    class_name: &str,
3277    params: &HashMap<String, Option<String>>,
3278    source: &[u8],
3279    instance_attr_types: &mut HashMap<(String, String), String>,
3280    attr_to_param_map: &mut HashMap<(String, String), String>,
3281) {
3282    let mut worklist = vec![root];
3283    while let Some(node) = worklist.pop() {
3284        let mut cursor = node.walk();
3285        for child in node.named_children(&mut cursor) {
3286            let ck = child.kind();
3287            if ck == "expression_statement" {
3288                // Look for assignment: this.X = Y
3289                let mut inner_cursor = child.walk();
3290                for inner in child.named_children(&mut inner_cursor) {
3291                    if inner.kind() == "assignment_expression" {
3292                        if let Some(left) = inner.child_by_field_name("left") {
3293                            if left.kind() == "member_expression" {
3294                                let obj = left
3295                                    .child_by_field_name("object")
3296                                    .and_then(|n| n.utf8_text(source).ok())
3297                                    .unwrap_or("");
3298                                let prop = left
3299                                    .child_by_field_name("property")
3300                                    .and_then(|n| n.utf8_text(source).ok())
3301                                    .unwrap_or("");
3302                                if obj == "this" && !prop.is_empty() {
3303                                    if let Some(right) = inner.child_by_field_name("right") {
3304                                        if right.kind() == "identifier" {
3305                                            let rhs_name = right.utf8_text(source).unwrap_or("");
3306                                            if params.contains_key(rhs_name) {
3307                                                attr_to_param_map.insert(
3308                                                    (class_name.to_string(), prop.to_string()),
3309                                                    rhs_name.to_string(),
3310                                                );
3311                                                if let Some(Some(type_hint)) = params.get(rhs_name)
3312                                                {
3313                                                    instance_attr_types.insert(
3314                                                        (class_name.to_string(), prop.to_string()),
3315                                                        type_hint.clone(),
3316                                                    );
3317                                                }
3318                                            }
3319                                        }
3320                                        if right.kind() == "new_expression" {
3321                                            if let Some(ctor) =
3322                                                right.child_by_field_name("constructor")
3323                                            {
3324                                                let name = ctor.utf8_text(source).unwrap_or("");
3325                                                if !name.is_empty() {
3326                                                    instance_attr_types.insert(
3327                                                        (class_name.to_string(), prop.to_string()),
3328                                                        name.to_string(),
3329                                                    );
3330                                                }
3331                                            }
3332                                        }
3333                                    }
3334                                }
3335                            }
3336                        }
3337                    }
3338                }
3339            }
3340            if ck == "statement_block" || ck == "block" {
3341                worklist.push(child);
3342            }
3343        }
3344    }
3345}
3346
3347/// Extract __init__ parameter names in order (excluding self).
3348fn extract_init_param_names_ordered(func_node: tree_sitter::Node, source: &[u8]) -> Vec<String> {
3349    let mut names = Vec::new();
3350    if let Some(params_node) = func_node.child_by_field_name("parameters") {
3351        let mut cursor = params_node.walk();
3352        for child in params_node.named_children(&mut cursor) {
3353            let param_name = if child.kind() == "identifier" {
3354                child.utf8_text(source).unwrap_or("").to_string()
3355            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter"
3356            {
3357                child
3358                    .child_by_field_name("name")
3359                    .or_else(|| child.named_child(0))
3360                    .and_then(|n| n.utf8_text(source).ok())
3361                    .unwrap_or("")
3362                    .to_string()
3363            } else {
3364                continue;
3365            };
3366            if param_name != "self" && param_name != "cls" && !param_name.is_empty() {
3367                names.push(param_name);
3368            }
3369        }
3370    }
3371    names
3372}
3373
3374fn extract_init_params(
3375    func_node: tree_sitter::Node,
3376    source: &[u8],
3377) -> HashMap<String, Option<String>> {
3378    let mut params = HashMap::new();
3379    if let Some(params_node) = func_node.child_by_field_name("parameters") {
3380        let mut cursor = params_node.walk();
3381        for child in params_node.named_children(&mut cursor) {
3382            let param_name = if child.kind() == "identifier" {
3383                child.utf8_text(source).unwrap_or("").to_string()
3384            } else if child.kind() == "typed_parameter" || child.kind() == "typed_default_parameter"
3385            {
3386                child
3387                    .child_by_field_name("name")
3388                    .or_else(|| child.named_child(0))
3389                    .and_then(|n| n.utf8_text(source).ok())
3390                    .unwrap_or("")
3391                    .to_string()
3392            } else {
3393                continue;
3394            };
3395            if param_name != "self" && param_name != "cls" {
3396                // Check for type annotation
3397                let type_hint = child
3398                    .child_by_field_name("type")
3399                    .and_then(|n| n.utf8_text(source).ok())
3400                    .map(|s| s.to_string());
3401                params.insert(param_name, type_hint);
3402            }
3403        }
3404    }
3405    params
3406}
3407
3408fn scan_init_body(
3409    root: tree_sitter::Node,
3410    class_name: &str,
3411    params: &HashMap<String, Option<String>>,
3412    source: &[u8],
3413    instance_attr_types: &mut HashMap<(String, String), String>,
3414    attr_to_param_map: &mut HashMap<(String, String), String>,
3415) {
3416    let mut worklist = vec![root];
3417    while let Some(node) = worklist.pop() {
3418        let mut cursor = node.walk();
3419        for child in node.named_children(&mut cursor) {
3420            if child.kind() == "expression_statement" || child.kind() == "assignment" {
3421                let assign = if child.kind() == "assignment" {
3422                    child
3423                } else {
3424                    let mut inner_cursor = child.walk();
3425                    let children: Vec<_> = child.named_children(&mut inner_cursor).collect();
3426                    match children.into_iter().find(|c| c.kind() == "assignment") {
3427                        Some(a) => a,
3428                        None => continue,
3429                    }
3430                };
3431
3432                if let Some(left) = assign.child_by_field_name("left") {
3433                    if left.kind() == "attribute" {
3434                        let obj = left
3435                            .child_by_field_name("object")
3436                            .and_then(|n| n.utf8_text(source).ok())
3437                            .unwrap_or("");
3438                        let attr = left
3439                            .child_by_field_name("attribute")
3440                            .and_then(|n| n.utf8_text(source).ok())
3441                            .unwrap_or("");
3442
3443                        if obj == "self" && !attr.is_empty() {
3444                            if let Some(right) = assign.child_by_field_name("right") {
3445                                if right.kind() == "identifier" {
3446                                    let rhs_name = right.utf8_text(source).unwrap_or("");
3447                                    // Record attr -> param mapping for later inference
3448                                    if params.contains_key(rhs_name) {
3449                                        attr_to_param_map.insert(
3450                                            (class_name.to_string(), attr.to_string()),
3451                                            rhs_name.to_string(),
3452                                        );
3453                                    }
3454                                    // If param has type hint, directly set the type
3455                                    if let Some(Some(type_hint)) = params.get(rhs_name) {
3456                                        instance_attr_types.insert(
3457                                            (class_name.to_string(), attr.to_string()),
3458                                            type_hint.clone(),
3459                                        );
3460                                    }
3461                                }
3462                                if right.kind() == "call" {
3463                                    if let Some(func) = right.child_by_field_name("function") {
3464                                        if func.kind() == "identifier" {
3465                                            let fname = func.utf8_text(source).unwrap_or("");
3466                                            if fname
3467                                                .chars()
3468                                                .next()
3469                                                .map_or(false, |c| c.is_uppercase())
3470                                            {
3471                                                instance_attr_types.insert(
3472                                                    (class_name.to_string(), attr.to_string()),
3473                                                    fname.to_string(),
3474                                                );
3475                                            }
3476                                        }
3477                                    }
3478                                }
3479                            }
3480                        }
3481                    }
3482                }
3483            }
3484            if child.kind() == "block" {
3485                worklist.push(child);
3486            }
3487        }
3488    }
3489}
3490
3491/// Infer constructor parameter types by analyzing call sites across all files.
3492/// For `Transaction(get_connection())`, we know get_connection() returns Connection,
3493/// so Transaction.__init__'s conn param has type Connection,
3494/// and self.conn in Transaction has type Connection.
3495fn infer_constructor_param_types(
3496    parsed_files: &[(String, String, tree_sitter::Tree)],
3497    return_type_map: &HashMap<String, String>,
3498    init_params: &HashMap<String, Vec<String>>,
3499    attr_to_param: &HashMap<(String, String), String>,
3500    symbol_table: &HashMap<String, Vec<String>>,
3501    instance_attr_types: &mut HashMap<(String, String), String>,
3502) {
3503    let func_name_returns = deterministic_return_types_by_name(return_type_map, symbol_table);
3504    let attr_to_param_index = build_attr_to_param_index(attr_to_param);
3505
3506    // Scan all files for constructor call sites: ClassName(arg1, arg2, ...)
3507    // Parallelized: each file produces local results, then merged.
3508    let local_results: Vec<HashMap<(String, String), String>> = maybe_par_iter!(parsed_files)
3509        .map(|(_file_path, content, tree)| {
3510            let source = content.as_bytes();
3511            let mut local_attr_types: HashMap<(String, String), String> = HashMap::new();
3512            scan_constructor_calls(
3513                tree.root_node(),
3514                source,
3515                &func_name_returns,
3516                init_params,
3517                &attr_to_param_index,
3518                &mut local_attr_types,
3519            );
3520            local_attr_types
3521        })
3522        .collect();
3523
3524    for local in local_results {
3525        let mut local_entries: Vec<((String, String), String)> = local.into_iter().collect();
3526        local_entries.sort_unstable();
3527        for (key, val) in local_entries {
3528            instance_attr_types.entry(key).or_insert(val);
3529        }
3530    }
3531}
3532
3533fn deterministic_return_types_by_name(
3534    return_type_map: &HashMap<String, String>,
3535    symbol_table: &HashMap<String, Vec<String>>,
3536) -> HashMap<String, String> {
3537    let mut by_name = HashMap::with_capacity(return_type_map.len());
3538    for (name, target_ids) in symbol_table {
3539        if let Some(return_type) = target_ids
3540            .iter()
3541            .find_map(|target_id| return_type_map.get(target_id))
3542        {
3543            by_name.insert(name.clone(), return_type.clone());
3544        }
3545    }
3546    by_name
3547}
3548
3549fn build_attr_to_param_index(
3550    attr_to_param: &HashMap<(String, String), String>,
3551) -> AttrToParamIndex<'_> {
3552    let mut index: AttrToParamIndex<'_> = HashMap::with_capacity(attr_to_param.len());
3553    for ((class_name, attr_name), param_name) in attr_to_param {
3554        index
3555            .entry((class_name.as_str(), param_name.as_str()))
3556            .or_default()
3557            .push((class_name.as_str(), attr_name.as_str()));
3558    }
3559    for attrs in index.values_mut() {
3560        attrs.sort_unstable();
3561    }
3562    index
3563}
3564
3565fn scan_constructor_calls(
3566    root: tree_sitter::Node,
3567    source: &[u8],
3568    func_name_returns: &HashMap<String, String>,
3569    init_params: &HashMap<String, Vec<String>>,
3570    attr_to_param_index: &AttrToParamIndex<'_>,
3571    instance_attr_types: &mut HashMap<(String, String), String>,
3572) {
3573    let mut worklist = vec![root];
3574    while let Some(node) = worklist.pop() {
3575        let kind = node.kind();
3576
3577        if kind == "call" {
3578            if let Some(func) = node.child_by_field_name("function") {
3579                if func.kind() == "identifier" {
3580                    let class_name = func.utf8_text(source).unwrap_or("");
3581                    // Only process uppercase names (constructor calls)
3582                    if class_name
3583                        .chars()
3584                        .next()
3585                        .map_or(false, |c| c.is_uppercase())
3586                    {
3587                        if let Some(param_names) = init_params.get(class_name) {
3588                            // Extract argument types
3589                            if let Some(args_node) = node.child_by_field_name("arguments") {
3590                                let mut arg_idx = 0;
3591                                let mut args_cursor = args_node.walk();
3592                                for arg in args_node.named_children(&mut args_cursor) {
3593                                    if arg_idx >= param_names.len() {
3594                                        break;
3595                                    }
3596                                    let param_name = &param_names[arg_idx];
3597
3598                                    // Try to infer the argument's type
3599                                    let arg_type = infer_expr_type(arg, source, func_name_returns);
3600
3601                                    if let Some(at) = arg_type {
3602                                        if let Some(attrs) = attr_to_param_index
3603                                            .get(&(class_name, param_name.as_str()))
3604                                        {
3605                                            for (cn, attr) in attrs {
3606                                                instance_attr_types
3607                                                    .entry(((*cn).to_string(), (*attr).to_string()))
3608                                                    .or_insert_with(|| at.clone());
3609                                            }
3610                                        }
3611                                    }
3612
3613                                    arg_idx += 1;
3614                                }
3615                            }
3616                        }
3617                    }
3618                }
3619            }
3620        }
3621
3622        push_named_children_rev(&mut worklist, node);
3623    }
3624}
3625
3626/// Infer the type of an expression node.
3627fn infer_expr_type(
3628    node: tree_sitter::Node,
3629    source: &[u8],
3630    func_name_returns: &HashMap<String, String>,
3631) -> Option<String> {
3632    match node.kind() {
3633        "call" => {
3634            if let Some(func) = node.child_by_field_name("function") {
3635                if func.kind() == "identifier" {
3636                    let name = func.utf8_text(source).unwrap_or("");
3637                    // Constructor call: Foo() -> type is Foo
3638                    if name.chars().next().map_or(false, |c| c.is_uppercase()) {
3639                        return Some(name.to_string());
3640                    }
3641                    // Function call with known return type
3642                    if let Some(ret) = func_name_returns.get(name) {
3643                        return Some(ret.clone());
3644                    }
3645                }
3646            }
3647            None
3648        }
3649        "identifier" => {
3650            // Could be a variable, but we don't have scope info here
3651            None
3652        }
3653        _ => None,
3654    }
3655}
3656
3657/// Resolve pending call types using the return type map.
3658/// For scopes with `x = func()` where func has a known return type, bind x to that type.
3659fn inject_return_type_bindings(
3660    scopes: &mut Vec<Scope>,
3661    func_name_return_types: &HashMap<String, String>,
3662    return_type_map: &HashMap<String, String>,
3663    import_table_by_name: &HashMap<&str, &str>,
3664) {
3665    // Resolve pending call types in all scopes
3666    for scope in scopes.iter_mut() {
3667        let resolved: Vec<(String, String)> = scope
3668            .pending_call_types
3669            .iter()
3670            .filter_map(|(var_name, func_name)| {
3671                import_table_by_name
3672                    .get(func_name.as_str())
3673                    .and_then(|target_id| return_type_map.get(*target_id))
3674                    .or_else(|| func_name_return_types.get(func_name))
3675                    .map(|ret_type| (var_name.clone(), ret_type.clone()))
3676            })
3677            .collect();
3678
3679        for (var_name, ret_type) in resolved {
3680            scope.types.insert(var_name, ret_type);
3681        }
3682    }
3683}
3684
3685fn build_ts_default_export_table(
3686    parsed_files: &[(String, String, tree_sitter::Tree)],
3687    symbol_table: &HashMap<String, Vec<String>>,
3688    entity_map: &HashMap<String, EntityInfo>,
3689) -> TsDefaultExportTable {
3690    // Per-file AST extraction is independent, so run it in parallel and merge.
3691    // Collecting preserves file order, so the merged result matches a sequential scan.
3692    let per_file: Vec<(Option<(String, String)>, Vec<TsDefaultReExport>)> =
3693        maybe_par_iter!(parsed_files)
3694            .filter_map(|(file_path, content, tree)| {
3695                if !is_js_ts_file(file_path) {
3696                    return None;
3697                }
3698
3699                let extracted = extract_ts_default_exports(tree.root_node(), content.as_bytes());
3700                let mut default_export: Option<(String, String)> = None;
3701                for name in extracted.names {
3702                    let Some(target_ids) = symbol_table.get(&name) else {
3703                        continue;
3704                    };
3705                    let target = target_ids.iter().find(|id| {
3706                        entity_map.get(*id).map_or(false, |entity| {
3707                            entity.file_path == *file_path && entity.parent_id.is_none()
3708                        })
3709                    });
3710                    if let Some(target_id) = target {
3711                        default_export = Some((file_path.clone(), target_id.clone()));
3712                    }
3713                }
3714
3715                let re_exports: Vec<TsDefaultReExport> = extracted
3716                    .re_exports
3717                    .into_iter()
3718                    .map(|(original_name, module_path)| TsDefaultReExport {
3719                        file_path: file_path.clone(),
3720                        original_name,
3721                        module_path,
3722                    })
3723                    .collect();
3724
3725                Some((default_export, re_exports))
3726            })
3727            .collect();
3728
3729    let mut default_exports = HashMap::new();
3730    let mut re_exports = Vec::new();
3731    for (default_export, file_re_exports) in per_file {
3732        if let Some((file_path, target_id)) = default_export {
3733            default_exports.insert(file_path, target_id);
3734        }
3735        re_exports.extend(file_re_exports);
3736    }
3737
3738    resolve_ts_default_re_exports(&mut default_exports, re_exports, symbol_table, entity_map);
3739    let sorted_files = sorted_default_export_files(&default_exports);
3740
3741    TsDefaultExportTable {
3742        exports_by_file: default_exports,
3743        sorted_files,
3744    }
3745}
3746
3747fn sorted_default_export_files(default_exports: &HashMap<String, String>) -> Vec<String> {
3748    let mut sorted_files: Vec<String> = default_exports.keys().cloned().collect();
3749    sort_import_candidate_files(&mut sorted_files, JS_TS_EXTENSIONS);
3750    sorted_files
3751}
3752
3753fn resolve_ts_default_re_exports(
3754    default_exports: &mut HashMap<String, String>,
3755    pending: Vec<TsDefaultReExport>,
3756    symbol_table: &HashMap<String, Vec<String>>,
3757    entity_map: &HashMap<String, EntityInfo>,
3758) {
3759    let mut pending = pending;
3760    while !pending.is_empty() {
3761        let sorted_files = sorted_default_export_files(default_exports);
3762        let mut unresolved = Vec::new();
3763        let mut progressed = false;
3764
3765        for re_export in pending {
3766            let target_id = if re_export.original_name == "default" {
3767                find_import_file(
3768                    &sorted_files,
3769                    &re_export.module_path,
3770                    &re_export.file_path,
3771                    JS_TS_EXTENSIONS,
3772                )
3773                .and_then(|target_file| default_exports.get(target_file))
3774                .cloned()
3775            } else {
3776                symbol_table
3777                    .get(&re_export.original_name)
3778                    .and_then(|target_ids| {
3779                        find_import_target(
3780                            target_ids,
3781                            &re_export.module_path,
3782                            &re_export.file_path,
3783                            JS_TS_EXTENSIONS,
3784                            entity_map,
3785                        )
3786                        .cloned()
3787                    })
3788            };
3789
3790            if let Some(target_id) = target_id {
3791                default_exports.insert(re_export.file_path, target_id);
3792                progressed = true;
3793            } else {
3794                unresolved.push(re_export);
3795            }
3796        }
3797
3798        if !progressed {
3799            break;
3800        }
3801        pending = unresolved;
3802    }
3803}
3804
3805fn build_top_level_entity_index(
3806    symbol_table: &HashMap<String, Vec<String>>,
3807    entity_map: &HashMap<String, EntityInfo>,
3808) -> TopLevelEntityIndex {
3809    let mut entities_by_file: HashMap<String, Vec<(String, String)>> = HashMap::new();
3810
3811    for (name, target_ids) in symbol_table {
3812        for target_id in target_ids {
3813            let Some(info) = entity_map.get(target_id) else {
3814                continue;
3815            };
3816            if !is_js_ts_file(&info.file_path) || info.parent_id.is_some() {
3817                continue;
3818            }
3819            entities_by_file
3820                .entry(info.file_path.clone())
3821                .or_default()
3822                .push((name.clone(), target_id.clone()));
3823        }
3824    }
3825
3826    let mut sorted_files: Vec<String> = entities_by_file.keys().cloned().collect();
3827    sort_import_candidate_files(&mut sorted_files, JS_TS_EXTENSIONS);
3828
3829    TopLevelEntityIndex {
3830        entities_by_file,
3831        sorted_files,
3832    }
3833}
3834
3835struct TsDefaultExports {
3836    names: Vec<String>,
3837    re_exports: Vec<(String, String)>,
3838}
3839
3840fn extract_ts_default_exports(root: tree_sitter::Node, source: &[u8]) -> TsDefaultExports {
3841    let mut names = Vec::new();
3842    let mut re_exports = Vec::new();
3843    let mut worklist = vec![root];
3844
3845    while let Some(node) = worklist.pop() {
3846        if node.kind() == "export_statement" {
3847            let has_source = node.child_by_field_name("source").is_some();
3848            let source_path = node
3849                .child_by_field_name("source")
3850                .and_then(|n| n.utf8_text(source).ok())
3851                .map(|text| {
3852                    text.trim_matches(|c: char| c == '\'' || c == '"')
3853                        .to_string()
3854                });
3855            let text = node.utf8_text(source).unwrap_or("");
3856            if !has_source {
3857                if let Some(declaration) = node.child_by_field_name("declaration") {
3858                    if text.contains("default") {
3859                        if let Some(name) = ts_default_declaration_name(declaration, source) {
3860                            names.push(name);
3861                        }
3862                    }
3863                } else if text.contains("default") && !has_ts_export_specifier(node) {
3864                    if let Some(name) = ts_bare_default_export_identifier(node, source) {
3865                        names.push(name);
3866                    }
3867                }
3868            }
3869            collect_ts_default_export_specifiers(
3870                node,
3871                source,
3872                source_path.as_deref(),
3873                &mut names,
3874                &mut re_exports,
3875            );
3876        }
3877
3878        let mut cursor = node.walk();
3879        for child in node.named_children(&mut cursor) {
3880            worklist.push(child);
3881        }
3882    }
3883
3884    TsDefaultExports { names, re_exports }
3885}
3886
3887fn ts_default_declaration_name(node: tree_sitter::Node, source: &[u8]) -> Option<String> {
3888    match node.kind() {
3889        "function_declaration"
3890        | "generator_function_declaration"
3891        | "class_declaration"
3892        | "abstract_class_declaration"
3893        | "lexical_declaration"
3894        | "variable_declaration" => ts_declaration_name(node, source),
3895        "identifier" => node.utf8_text(source).ok().map(str::to_string),
3896        _ => None,
3897    }
3898}
3899
3900fn has_ts_export_specifier(node: tree_sitter::Node) -> bool {
3901    let mut worklist = vec![node];
3902    while let Some(current) = worklist.pop() {
3903        let mut cursor = current.walk();
3904        for child in current.named_children(&mut cursor) {
3905            if child.kind() == "export_specifier" {
3906                return true;
3907            }
3908            worklist.push(child);
3909        }
3910    }
3911    false
3912}
3913
3914fn collect_ts_default_export_specifiers(
3915    node: tree_sitter::Node,
3916    source: &[u8],
3917    source_path: Option<&str>,
3918    names: &mut Vec<String>,
3919    re_exports: &mut Vec<(String, String)>,
3920) {
3921    let mut worklist = vec![node];
3922    while let Some(current) = worklist.pop() {
3923        let mut cursor = current.walk();
3924        for child in current.named_children(&mut cursor) {
3925            if child.kind() == "export_specifier" {
3926                let original = child
3927                    .child_by_field_name("name")
3928                    .and_then(|n| n.utf8_text(source).ok())
3929                    .unwrap_or("");
3930                let local = child
3931                    .child_by_field_name("alias")
3932                    .and_then(|n| n.utf8_text(source).ok())
3933                    .unwrap_or(original);
3934                if local == "default" && !original.is_empty() {
3935                    if let Some(source_path) = source_path {
3936                        re_exports.push((original.to_string(), source_path.to_string()));
3937                    } else {
3938                        names.push(original.to_string());
3939                    }
3940                }
3941            } else {
3942                worklist.push(child);
3943            }
3944        }
3945    }
3946}
3947
3948fn ts_declaration_name(node: tree_sitter::Node, source: &[u8]) -> Option<String> {
3949    if let Some(name) = node.child_by_field_name("name") {
3950        return Some(name.utf8_text(source).ok()?.to_string());
3951    }
3952
3953    if node.kind() == "lexical_declaration" || node.kind() == "variable_declaration" {
3954        let mut cursor = node.walk();
3955        for child in node.named_children(&mut cursor) {
3956            if child.kind() == "variable_declarator" {
3957                if let Some(name) = child.child_by_field_name("name") {
3958                    return Some(name.utf8_text(source).ok()?.to_string());
3959                }
3960            }
3961        }
3962    }
3963
3964    let mut cursor = node.walk();
3965    let name = node
3966        .named_children(&mut cursor)
3967        .find(|child| matches!(child.kind(), "identifier" | "type_identifier"))
3968        .and_then(|child| child.utf8_text(source).ok())
3969        .map(str::to_string);
3970    name
3971}
3972
3973fn ts_bare_default_export_identifier(node: tree_sitter::Node, source: &[u8]) -> Option<String> {
3974    let text = node.utf8_text(source).ok()?.trim();
3975    let rest = text.strip_prefix("export")?.trim_start();
3976    let rest = rest.strip_prefix("default")?.trim_start();
3977    let name_end = js_ts_identifier_end(rest)?;
3978    let name = &rest[..name_end];
3979    let trailing = rest[name_end..].trim_start();
3980    only_js_ts_statement_trivia(trailing).then(|| name.to_string())
3981}
3982
3983fn js_ts_identifier_end(text: &str) -> Option<usize> {
3984    let mut chars = text.char_indices();
3985    let (_, first) = chars.next()?;
3986    if !(first == '_' || first == '$' || first.is_ascii_alphabetic()) {
3987        return None;
3988    }
3989
3990    let mut end = first.len_utf8();
3991    for (idx, ch) in chars {
3992        if ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() {
3993            end = idx + ch.len_utf8();
3994        } else {
3995            break;
3996        }
3997    }
3998    Some(end)
3999}
4000
4001fn only_js_ts_statement_trivia(mut text: &str) -> bool {
4002    loop {
4003        text = text.trim_start();
4004        if let Some(rest) = text.strip_prefix(';') {
4005            text = rest;
4006            continue;
4007        }
4008        if text.is_empty() {
4009            return true;
4010        }
4011        if text.starts_with("//") {
4012            return true;
4013        }
4014        if let Some(rest) = text.strip_prefix("/*") {
4015            let Some(end) = rest.find("*/") else {
4016                return false;
4017            };
4018            text = &rest[end + 2..];
4019            continue;
4020        }
4021        return false;
4022    }
4023}
4024
4025/// Extract import statements from the AST.
4026fn extract_imports_from_ast<'a>(
4027    root: tree_sitter::Node,
4028    file_path: &str,
4029    source: &[u8],
4030    symbol_table: &HashMap<String, Vec<String>>,
4031    entity_map: &HashMap<String, EntityInfo>,
4032    import_table: &mut HashMap<(String, String), String>,
4033    scopes: &mut Vec<Scope>,
4034    config: &ScopeResolveConfig,
4035    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
4036    ts_default_exports: &TsDefaultExportTable,
4037    top_level_entities: &OnceLock<TopLevelEntityIndex>,
4038    parsed_files: &'a [(String, String, tree_sitter::Tree)],
4039    content_by_file: &OnceLock<HashMap<&'a str, &'a str>>,
4040    exported_names_by_file: &Mutex<HashMap<String, Arc<HashSet<String>>>>,
4041    skip_js_ts_imports: bool,
4042) {
4043    let mut worklist = vec![root];
4044    while let Some(node) = worklist.pop() {
4045        let mut cursor = node.walk();
4046        for child in node.named_children(&mut cursor) {
4047            let ck = child.kind();
4048            let handled = match ck {
4049                "import_from_statement" => {
4050                    extract_python_import(
4051                        child,
4052                        file_path,
4053                        source,
4054                        symbol_table,
4055                        entity_map,
4056                        import_table,
4057                        scopes,
4058                    );
4059                    true
4060                }
4061                "import_statement"
4062                    if config.self_keywords.contains(&"self")
4063                        && config.self_keywords.contains(&"cls") =>
4064                {
4065                    // Python: `import mod` or `import mod as m`
4066                    extract_python_module_import(
4067                        child,
4068                        file_path,
4069                        source,
4070                        symbol_table,
4071                        entity_map,
4072                        import_table,
4073                        scopes,
4074                    );
4075                    true
4076                }
4077                "import_statement" if !config.self_keywords.contains(&"cls") => {
4078                    if !skip_js_ts_imports {
4079                        extract_ts_import(
4080                            child,
4081                            file_path,
4082                            source,
4083                            symbol_table,
4084                            entity_map,
4085                            import_table,
4086                            scopes,
4087                            ts_default_exports,
4088                            top_level_entities,
4089                            parsed_files,
4090                            content_by_file,
4091                            exported_names_by_file,
4092                        );
4093                    }
4094                    true
4095                }
4096                "export_statement" if !config.self_keywords.contains(&"cls") => {
4097                    if !skip_js_ts_imports {
4098                        extract_ts_re_export(
4099                            child,
4100                            file_path,
4101                            source,
4102                            symbol_table,
4103                            entity_map,
4104                            import_table,
4105                            scopes,
4106                            ts_default_exports,
4107                        );
4108                    }
4109                    true
4110                }
4111                "use_declaration" => {
4112                    extract_rust_use(
4113                        child,
4114                        file_path,
4115                        source,
4116                        symbol_table,
4117                        entity_map,
4118                        import_table,
4119                        scopes,
4120                    );
4121                    true
4122                }
4123                "import_declaration" => {
4124                    extract_go_import(
4125                        child,
4126                        file_path,
4127                        source,
4128                        symbol_table,
4129                        entity_map,
4130                        import_table,
4131                        scopes,
4132                        go_pkg_index,
4133                    );
4134                    true
4135                }
4136                _ => false,
4137            };
4138            if !handled {
4139                worklist.push(child);
4140            }
4141        }
4142    }
4143}
4144
4145/// TS: `import { Foo, Bar } from './module'` or `import Foo from './module'`
4146fn extract_ts_import<'a>(
4147    node: tree_sitter::Node,
4148    file_path: &str,
4149    source: &[u8],
4150    symbol_table: &HashMap<String, Vec<String>>,
4151    entity_map: &HashMap<String, EntityInfo>,
4152    import_table: &mut HashMap<(String, String), String>,
4153    scopes: &mut Vec<Scope>,
4154    ts_default_exports: &TsDefaultExportTable,
4155    top_level_entities: &OnceLock<TopLevelEntityIndex>,
4156    parsed_files: &'a [(String, String, tree_sitter::Tree)],
4157    content_by_file: &OnceLock<HashMap<&'a str, &'a str>>,
4158    exported_names_by_file: &Mutex<HashMap<String, Arc<HashSet<String>>>>,
4159) {
4160    // Extract the source module from the `from '...'` clause
4161    let source_path = node
4162        .child_by_field_name("source")
4163        .and_then(|n| n.utf8_text(source).ok())
4164        .unwrap_or("")
4165        .trim_matches(|c: char| c == '\'' || c == '"');
4166
4167    if source_path.is_empty() {
4168        return;
4169    }
4170
4171    // Walk children to find import clause
4172    let mut cursor = node.walk();
4173    for child in node.named_children(&mut cursor) {
4174        if child.kind() == "import_clause" {
4175            let mut clause_cursor = child.walk();
4176            for clause_child in child.named_children(&mut clause_cursor) {
4177                if clause_child.kind() == "named_imports" {
4178                    // { Foo, Bar as Baz }
4179                    let mut imports_cursor = clause_child.walk();
4180                    for spec in clause_child.named_children(&mut imports_cursor) {
4181                        if spec.kind() == "import_specifier" {
4182                            let original = spec
4183                                .child_by_field_name("name")
4184                                .and_then(|n| n.utf8_text(source).ok())
4185                                .unwrap_or("");
4186                            let local = spec
4187                                .child_by_field_name("alias")
4188                                .and_then(|n| n.utf8_text(source).ok())
4189                                .unwrap_or(original);
4190
4191                            if !original.is_empty() {
4192                                resolve_import_name(
4193                                    original,
4194                                    local,
4195                                    source_path,
4196                                    file_path,
4197                                    JS_TS_EXTENSIONS,
4198                                    symbol_table,
4199                                    entity_map,
4200                                    import_table,
4201                                    scopes,
4202                                );
4203                            }
4204                        }
4205                    }
4206                } else if clause_child.kind() == "namespace_import" {
4207                    // import * as m from './module'
4208                    // Register exported source module entities so m.foo() resolves.
4209                    let mut ns_cursor = clause_child.walk();
4210                    let alias = clause_child
4211                        .child_by_field_name("alias")
4212                        .or_else(|| {
4213                            clause_child
4214                                .named_children(&mut ns_cursor)
4215                                .find(|c| c.kind() == "identifier")
4216                        })
4217                        .and_then(|n| n.utf8_text(source).ok())
4218                        .unwrap_or("");
4219                    if !alias.is_empty() {
4220                        register_ts_namespace_import(
4221                            alias,
4222                            source_path,
4223                            file_path,
4224                            JS_TS_EXTENSIONS,
4225                            top_level_entities,
4226                            symbol_table,
4227                            entity_map,
4228                            parsed_files,
4229                            content_by_file,
4230                            exported_names_by_file,
4231                            import_table,
4232                            scopes,
4233                        );
4234                    }
4235                } else if clause_child.kind() == "identifier" {
4236                    // Default import: import Foo from './module'
4237                    let name = clause_child.utf8_text(source).unwrap_or("");
4238                    if !name.is_empty() {
4239                        resolve_default_import(
4240                            name,
4241                            source_path,
4242                            file_path,
4243                            JS_TS_EXTENSIONS,
4244                            ts_default_exports,
4245                            import_table,
4246                            scopes,
4247                        );
4248                    }
4249                }
4250            }
4251        }
4252    }
4253}
4254
4255fn extract_ts_re_export(
4256    node: tree_sitter::Node,
4257    file_path: &str,
4258    source: &[u8],
4259    symbol_table: &HashMap<String, Vec<String>>,
4260    entity_map: &HashMap<String, EntityInfo>,
4261    import_table: &mut HashMap<(String, String), String>,
4262    scopes: &mut Vec<Scope>,
4263    ts_default_exports: &TsDefaultExportTable,
4264) {
4265    let source_path = node
4266        .child_by_field_name("source")
4267        .and_then(|n| n.utf8_text(source).ok())
4268        .unwrap_or("")
4269        .trim_matches(|c: char| c == '\'' || c == '"');
4270
4271    if source_path.is_empty() {
4272        return;
4273    }
4274
4275    let mut worklist = vec![node];
4276    while let Some(current) = worklist.pop() {
4277        let mut cursor = current.walk();
4278        for child in current.named_children(&mut cursor) {
4279            match child.kind() {
4280                "export_specifier" => {
4281                    let original = child
4282                        .child_by_field_name("name")
4283                        .and_then(|n| n.utf8_text(source).ok())
4284                        .unwrap_or("");
4285                    let local = child
4286                        .child_by_field_name("alias")
4287                        .and_then(|n| n.utf8_text(source).ok())
4288                        .unwrap_or(original);
4289
4290                    if original.is_empty() || local.is_empty() {
4291                        continue;
4292                    }
4293
4294                    if original == "default" {
4295                        resolve_default_import(
4296                            local,
4297                            source_path,
4298                            file_path,
4299                            JS_TS_EXTENSIONS,
4300                            ts_default_exports,
4301                            import_table,
4302                            scopes,
4303                        );
4304                    } else {
4305                        resolve_import_name(
4306                            original,
4307                            local,
4308                            source_path,
4309                            file_path,
4310                            JS_TS_EXTENSIONS,
4311                            symbol_table,
4312                            entity_map,
4313                            import_table,
4314                            scopes,
4315                        );
4316                    }
4317                }
4318                "export_clause" | "namespace_export" => {
4319                    worklist.push(child);
4320                }
4321                _ => {}
4322            }
4323        }
4324    }
4325}
4326
4327/// Rust: `use crate::module::Name;` or `use crate::module::{A, B};`
4328/// Parse from the text of the use_declaration for reliability.
4329fn extract_rust_use(
4330    node: tree_sitter::Node,
4331    file_path: &str,
4332    source: &[u8],
4333    symbol_table: &HashMap<String, Vec<String>>,
4334    entity_map: &HashMap<String, EntityInfo>,
4335    import_table: &mut HashMap<(String, String), String>,
4336    scopes: &mut Vec<Scope>,
4337) {
4338    let text = node.utf8_text(source).unwrap_or("").trim().to_string();
4339    // Strip `use ` prefix and trailing `;`
4340    let text = text.strip_prefix("use ").unwrap_or(&text);
4341    let text = text.strip_prefix("pub use ").unwrap_or(text);
4342    let text = text.trim_end_matches(';').trim();
4343
4344    // Strip crate/super/self prefix
4345    let text = text
4346        .strip_prefix("crate::")
4347        .or_else(|| text.strip_prefix("super::"))
4348        .or_else(|| text.strip_prefix("self::"))
4349        .unwrap_or(text);
4350
4351    // Check for grouped import: module::{A, B, C}
4352    if let Some(brace_pos) = text.find("::{") {
4353        let module_path = &text[..brace_pos];
4354        let source_module = module_path.rsplit("::").next().unwrap_or(module_path);
4355
4356        let names_part = &text[brace_pos + 3..];
4357        let names_part = names_part.trim_end_matches('}');
4358
4359        for name_part in names_part.split(',') {
4360            let name_part = name_part.trim();
4361            if name_part.is_empty() {
4362                continue;
4363            }
4364            let (original, local) = if let Some(pos) = name_part.find(" as ") {
4365                (name_part[..pos].trim(), name_part[pos + 4..].trim())
4366            } else {
4367                (name_part, name_part)
4368            };
4369            if !original.is_empty() {
4370                resolve_import_name(
4371                    original,
4372                    local,
4373                    source_module,
4374                    file_path,
4375                    &[".rs"],
4376                    symbol_table,
4377                    entity_map,
4378                    import_table,
4379                    scopes,
4380                );
4381            }
4382        }
4383    } else {
4384        // Simple import: module::Name
4385        let parts: Vec<&str> = text.split("::").collect();
4386        if parts.is_empty() {
4387            return;
4388        }
4389        let imported_name = parts.last().unwrap().trim();
4390        let (original, local) = if let Some(pos) = imported_name.find(" as ") {
4391            (&imported_name[..pos], imported_name[pos + 4..].trim())
4392        } else {
4393            (imported_name, imported_name)
4394        };
4395        let source_module = if parts.len() >= 2 {
4396            parts[parts.len() - 2]
4397        } else {
4398            parts[0]
4399        };
4400        if !original.is_empty() && !source_module.is_empty() {
4401            resolve_import_name(
4402                original,
4403                local,
4404                source_module,
4405                file_path,
4406                &[".rs"],
4407                symbol_table,
4408                entity_map,
4409                import_table,
4410                scopes,
4411            );
4412        }
4413    }
4414}
4415
4416/// Go: `import ("module/path")` - maps package names to entities
4417fn extract_go_import(
4418    node: tree_sitter::Node,
4419    file_path: &str,
4420    source: &[u8],
4421    symbol_table: &HashMap<String, Vec<String>>,
4422    entity_map: &HashMap<String, EntityInfo>,
4423    import_table: &mut HashMap<(String, String), String>,
4424    scopes: &mut Vec<Scope>,
4425    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
4426) {
4427    let mut cursor = node.walk();
4428    for child in node.named_children(&mut cursor) {
4429        if child.kind() == "import_spec" || child.kind() == "import_spec_list" {
4430            extract_go_import_specs(
4431                child,
4432                file_path,
4433                source,
4434                symbol_table,
4435                entity_map,
4436                import_table,
4437                scopes,
4438                go_pkg_index,
4439            );
4440        } else if child.kind() == "interpreted_string_literal"
4441            || child.kind() == "raw_string_literal"
4442        {
4443            let path = child
4444                .utf8_text(source)
4445                .unwrap_or("")
4446                .trim_matches('"')
4447                .trim_matches('`');
4448            let pkg_name = path.rsplit('/').next().unwrap_or(path);
4449            register_go_package_imports(
4450                pkg_name,
4451                file_path,
4452                symbol_table,
4453                entity_map,
4454                import_table,
4455                scopes,
4456                go_pkg_index,
4457            );
4458        }
4459    }
4460}
4461
4462fn extract_go_import_specs(
4463    root: tree_sitter::Node,
4464    file_path: &str,
4465    source: &[u8],
4466    symbol_table: &HashMap<String, Vec<String>>,
4467    entity_map: &HashMap<String, EntityInfo>,
4468    import_table: &mut HashMap<(String, String), String>,
4469    scopes: &mut Vec<Scope>,
4470    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
4471) {
4472    let mut worklist = vec![root];
4473    while let Some(node) = worklist.pop() {
4474        let mut cursor = node.walk();
4475        for child in node.named_children(&mut cursor) {
4476            if child.kind() == "import_spec" {
4477                let path_node = child
4478                    .child_by_field_name("path")
4479                    .or_else(|| child.named_child(0));
4480                if let Some(pn) = path_node {
4481                    let path = pn
4482                        .utf8_text(source)
4483                        .unwrap_or("")
4484                        .trim_matches('"')
4485                        .trim_matches('`');
4486                    let pkg_name = path.rsplit('/').next().unwrap_or(path);
4487                    register_go_package_imports(
4488                        pkg_name,
4489                        file_path,
4490                        symbol_table,
4491                        entity_map,
4492                        import_table,
4493                        scopes,
4494                        go_pkg_index,
4495                    );
4496                }
4497            } else {
4498                worklist.push(child);
4499            }
4500        }
4501    }
4502}
4503
4504fn register_go_package_imports(
4505    pkg_name: &str,
4506    file_path: &str,
4507    _symbol_table: &HashMap<String, Vec<String>>,
4508    _entity_map: &HashMap<String, EntityInfo>,
4509    import_table: &mut HashMap<(String, String), String>,
4510    scopes: &mut Vec<Scope>,
4511    go_pkg_index: &HashMap<String, Vec<(String, String)>>,
4512) {
4513    // Use pre-built package index for O(1) lookup instead of O(symbol_table) scan
4514    if let Some(entries) = go_pkg_index.get(pkg_name) {
4515        for (name, target_id) in entries {
4516            import_table.insert((file_path.to_string(), name.clone()), target_id.clone());
4517            if !scopes.is_empty() {
4518                scopes[0].defs.insert(name.clone(), target_id.clone());
4519            }
4520        }
4521    }
4522}
4523
4524/// Shared helper: resolve an imported name against the symbol table
4525fn resolve_import_name(
4526    original_name: &str,
4527    local_name: &str,
4528    source_path: &str,
4529    file_path: &str,
4530    extensions: &[&str],
4531    symbol_table: &HashMap<String, Vec<String>>,
4532    entity_map: &HashMap<String, EntityInfo>,
4533    import_table: &mut HashMap<(String, String), String>,
4534    scopes: &mut Vec<Scope>,
4535) {
4536    if let Some(target_ids) = symbol_table.get(original_name) {
4537        let target = find_import_target(target_ids, source_path, file_path, extensions, entity_map);
4538
4539        if let Some(target_id) = target {
4540            import_table.insert(
4541                (file_path.to_string(), local_name.to_string()),
4542                target_id.clone(),
4543            );
4544            if !scopes.is_empty() {
4545                scopes[0]
4546                    .defs
4547                    .insert(local_name.to_string(), target_id.clone());
4548            }
4549        }
4550    }
4551}
4552
4553fn resolve_default_import(
4554    local_name: &str,
4555    source_path: &str,
4556    file_path: &str,
4557    extensions: &[&str],
4558    default_exports: &TsDefaultExportTable,
4559    import_table: &mut HashMap<(String, String), String>,
4560    scopes: &mut Vec<Scope>,
4561) {
4562    let target = find_import_file(
4563        &default_exports.sorted_files,
4564        source_path,
4565        file_path,
4566        extensions,
4567    )
4568    .and_then(|target_file| default_exports.exports_by_file.get(target_file))
4569    .cloned();
4570
4571    if let Some(target_id) = target {
4572        import_table.insert(
4573            (file_path.to_string(), local_name.to_string()),
4574            target_id.clone(),
4575        );
4576        if !scopes.is_empty() {
4577            scopes[0].defs.insert(local_name.to_string(), target_id);
4578        }
4579    }
4580}
4581
4582/// Register exported source module entities under a namespace alias.
4583/// For `import * as m from './module'`, exported entities from the module
4584/// are registered so that `m.foo()` resolves via the method call path.
4585fn register_ts_namespace_import<'a>(
4586    alias: &str,
4587    source_path: &str,
4588    file_path: &str,
4589    extensions: &[&str],
4590    top_level_entities: &OnceLock<TopLevelEntityIndex>,
4591    symbol_table: &HashMap<String, Vec<String>>,
4592    entity_map: &HashMap<String, EntityInfo>,
4593    parsed_files: &'a [(String, String, tree_sitter::Tree)],
4594    content_by_file: &OnceLock<HashMap<&'a str, &'a str>>,
4595    exported_names_by_file: &Mutex<HashMap<String, Arc<HashSet<String>>>>,
4596    import_table: &mut HashMap<(String, String), String>,
4597    _scopes: &mut Vec<Scope>,
4598) {
4599    let top_level_entities =
4600        top_level_entities.get_or_init(|| build_top_level_entity_index(symbol_table, entity_map));
4601    let Some(candidate_file) = find_import_file(
4602        &top_level_entities.sorted_files,
4603        source_path,
4604        file_path,
4605        extensions,
4606    ) else {
4607        return;
4608    };
4609    let Some(entries) = top_level_entities.entities_by_file.get(candidate_file) else {
4610        return;
4611    };
4612    let exported_names = {
4613        let mut cache = exported_names_by_file.lock().unwrap();
4614        cache
4615            .entry(candidate_file.to_string())
4616            .or_insert_with(|| {
4617                let content_by_file = content_by_file.get_or_init(|| {
4618                    parsed_files
4619                        .iter()
4620                        .map(|(file_path, content, _)| (file_path.as_str(), content.as_str()))
4621                        .collect()
4622                });
4623                Arc::new(
4624                    content_by_file
4625                        .get(candidate_file)
4626                        .map(|content| js_ts_named_exports_from_content(content))
4627                        .unwrap_or_default(),
4628                )
4629            })
4630            .clone()
4631    };
4632    for (name, target_id) in entries {
4633        if !exported_names.contains(name) {
4634            continue;
4635        }
4636        let qualified_name = format!("{alias}.{name}");
4637        import_table
4638            .entry((file_path.to_string(), qualified_name))
4639            .or_insert_with(|| target_id.clone());
4640    }
4641}
4642
4643fn register_namespace_import(
4644    alias: &str,
4645    source_path: &str,
4646    file_path: &str,
4647    extensions: &[&str],
4648    symbol_table: &HashMap<String, Vec<String>>,
4649    entity_map: &HashMap<String, EntityInfo>,
4650    import_table: &mut HashMap<(String, String), String>,
4651    _scopes: &mut Vec<Scope>,
4652) {
4653    // Find all top-level entities whose file matches the imported module.
4654    for (name, target_ids) in symbol_table {
4655        for target_id in target_ids {
4656            if let Some(info) = entity_map.get(target_id) {
4657                if import_source_matches_file(file_path, source_path, extensions, &info.file_path)
4658                    && info.parent_id.is_none()
4659                {
4660                    let qualified_name = format!("{alias}.{name}");
4661                    import_table.insert(
4662                        (file_path.to_string(), qualified_name.clone()),
4663                        target_id.clone(),
4664                    );
4665                }
4666            }
4667        }
4668    }
4669}
4670
4671fn extract_python_import(
4672    node: tree_sitter::Node,
4673    file_path: &str,
4674    source: &[u8],
4675    symbol_table: &HashMap<String, Vec<String>>,
4676    entity_map: &HashMap<String, EntityInfo>,
4677    import_table: &mut HashMap<(String, String), String>,
4678    scopes: &mut Vec<Scope>,
4679) {
4680    // import_from_statement has:
4681    //   module_name (dotted_name or relative_import)
4682    //   name fields (imported names)
4683    let module_node = node.child_by_field_name("module_name");
4684    let module_name = module_node
4685        .and_then(|n| n.utf8_text(source).ok())
4686        .unwrap_or("");
4687
4688    // Walk children to find imported names
4689    let mut cursor = node.walk();
4690    for child in node.named_children(&mut cursor) {
4691        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
4692            let (original, local) = if child.kind() == "aliased_import" {
4693                let orig = child
4694                    .child_by_field_name("name")
4695                    .and_then(|n| n.utf8_text(source).ok())
4696                    .unwrap_or("");
4697                let alias = child
4698                    .child_by_field_name("alias")
4699                    .and_then(|n| n.utf8_text(source).ok())
4700                    .unwrap_or(orig);
4701                (orig, alias)
4702            } else {
4703                let name = child.utf8_text(source).unwrap_or("");
4704                (name, name)
4705            };
4706
4707            if original.is_empty() {
4708                continue;
4709            }
4710
4711            resolve_import_name(
4712                original,
4713                local,
4714                module_name,
4715                file_path,
4716                &[".py"],
4717                symbol_table,
4718                entity_map,
4719                import_table,
4720                scopes,
4721            );
4722        }
4723    }
4724}
4725
4726/// Python: `import mod` or `import mod as m` — registers all entities from
4727/// the module so that `m.foo()` resolves via the method-call path.
4728fn extract_python_module_import(
4729    node: tree_sitter::Node,
4730    file_path: &str,
4731    source: &[u8],
4732    symbol_table: &HashMap<String, Vec<String>>,
4733    entity_map: &HashMap<String, EntityInfo>,
4734    import_table: &mut HashMap<(String, String), String>,
4735    scopes: &mut Vec<Scope>,
4736) {
4737    let mut cursor = node.walk();
4738    for child in node.named_children(&mut cursor) {
4739        let (module_name, _alias) = match child.kind() {
4740            "dotted_name" => {
4741                let name = child.utf8_text(source).unwrap_or("");
4742                (name, name)
4743            }
4744            "aliased_import" => {
4745                let orig = child
4746                    .child_by_field_name("name")
4747                    .and_then(|n| n.utf8_text(source).ok())
4748                    .unwrap_or("");
4749                let alias = child
4750                    .child_by_field_name("alias")
4751                    .and_then(|n| n.utf8_text(source).ok())
4752                    .unwrap_or(orig);
4753                (orig, alias)
4754            }
4755            _ => continue,
4756        };
4757
4758        if module_name.is_empty() {
4759            continue;
4760        }
4761
4762        // Register all entities from the source module
4763        register_namespace_import(
4764            _alias,
4765            module_name,
4766            file_path,
4767            &[".py"],
4768            symbol_table,
4769            entity_map,
4770            import_table,
4771            scopes,
4772        );
4773    }
4774}
4775
4776fn build_swift_call_signatures(
4777    parsed_files: &[(String, String, tree_sitter::Tree)],
4778    all_entities: &[SemanticEntity],
4779    entity_ranges: &HashMap<String, Vec<(usize, usize, String)>>,
4780    entity_map: &HashMap<String, EntityInfo>,
4781) -> HashMap<String, SwiftCallSignature> {
4782    let mut signatures = HashMap::new();
4783
4784    for (file_path, content, tree) in parsed_files {
4785        if !file_path.ends_with(".swift") {
4786            continue;
4787        }
4788
4789        let Some(ranges) = entity_ranges.get(file_path.as_str()) else {
4790            continue;
4791        };
4792
4793        let source = content.as_bytes();
4794        let mut worklist = vec![tree.root_node()];
4795        while let Some(node) = worklist.pop() {
4796            if matches!(node.kind(), "function_declaration" | "init_declaration") {
4797                if let Some(entity_id) =
4798                    find_entity_id_for_swift_declaration(node, ranges, entity_map)
4799                {
4800                    let argument_labels = extract_swift_declaration_argument_labels(node, source);
4801                    signatures.insert(entity_id, SwiftCallSignature { argument_labels });
4802                }
4803            }
4804
4805            push_named_children_rev(&mut worklist, node);
4806        }
4807    }
4808
4809    let mut content_parser: Option<tree_sitter::Parser> = None;
4810    for entity in all_entities {
4811        if signatures.contains_key(&entity.id) || !is_swift_callable_entity_info(entity) {
4812            continue;
4813        }
4814
4815        if content_parser.is_none() {
4816            content_parser = swift_signature_parser();
4817        }
4818        let Some(parser) = content_parser.as_mut() else {
4819            break;
4820        };
4821
4822        if let Some(argument_labels) = extract_swift_signature_from_entity_content(entity, parser) {
4823            signatures.insert(entity.id.clone(), SwiftCallSignature { argument_labels });
4824        }
4825    }
4826
4827    signatures
4828}
4829
4830fn find_entity_id_for_swift_declaration(
4831    node: tree_sitter::Node,
4832    ranges: &[(usize, usize, String)],
4833    entity_map: &HashMap<String, EntityInfo>,
4834) -> Option<String> {
4835    let start_line = node.start_position().row + 1;
4836    let end_line = node.end_position().row + 1;
4837
4838    ranges
4839        .iter()
4840        .filter(|(start, end, id)| {
4841            *start <= start_line
4842                && *end >= end_line
4843                && entity_map.get(id).map_or(false, is_swift_callable_entity)
4844        })
4845        .min_by_key(|(start, end, _)| end.saturating_sub(*start))
4846        .map(|(_, _, id)| id.clone())
4847}
4848
4849fn is_swift_callable_entity(info: &EntityInfo) -> bool {
4850    info.file_path.ends_with(".swift")
4851        && matches!(
4852            info.entity_type.as_str(),
4853            "function" | "method" | "init" | "init_declaration"
4854        )
4855}
4856
4857fn is_swift_callable_entity_info(entity: &SemanticEntity) -> bool {
4858    entity.file_path.ends_with(".swift")
4859        && matches!(
4860            entity.entity_type.as_str(),
4861            "function" | "method" | "init" | "init_declaration"
4862        )
4863}
4864
4865fn swift_signature_parser() -> Option<tree_sitter::Parser> {
4866    let language = get_language_config(".swift").and_then(|config| (config.get_language)())?;
4867    let mut parser = tree_sitter::Parser::new();
4868    parser.set_language(&language).ok()?;
4869    Some(parser)
4870}
4871
4872fn extract_swift_signature_from_entity_content(
4873    entity: &SemanticEntity,
4874    parser: &mut tree_sitter::Parser,
4875) -> Option<Vec<Option<String>>> {
4876    if let Some(argument_labels) = parse_swift_signature_source(parser, &entity.content) {
4877        return Some(argument_labels);
4878    }
4879
4880    if matches!(entity.entity_type.as_str(), "init" | "init_declaration") {
4881        let wrapped = format!("struct __SemSignature {{\n{}\n}}\n", entity.content);
4882        parse_swift_signature_source(parser, &wrapped)
4883    } else {
4884        None
4885    }
4886}
4887
4888fn parse_swift_signature_source(
4889    parser: &mut tree_sitter::Parser,
4890    source_text: &str,
4891) -> Option<Vec<Option<String>>> {
4892    let tree = parser.parse(source_text.as_bytes(), None)?;
4893    let source = source_text.as_bytes();
4894    find_first_swift_callable_declaration(tree.root_node())
4895        .map(|node| extract_swift_declaration_argument_labels(node, source))
4896}
4897
4898fn find_first_swift_callable_declaration<'a>(
4899    root: tree_sitter::Node<'a>,
4900) -> Option<tree_sitter::Node<'a>> {
4901    let mut worklist = vec![root];
4902    while let Some(node) = worklist.pop() {
4903        if matches!(node.kind(), "function_declaration" | "init_declaration") {
4904            return Some(node);
4905        }
4906
4907        push_named_children_rev(&mut worklist, node);
4908    }
4909
4910    None
4911}
4912
4913fn extract_swift_declaration_argument_labels(
4914    node: tree_sitter::Node,
4915    source: &[u8],
4916) -> Vec<Option<String>> {
4917    let mut labels = Vec::new();
4918    let mut worklist = vec![node];
4919
4920    while let Some(current) = worklist.pop() {
4921        if current.kind() == "function_body" {
4922            continue;
4923        }
4924
4925        if current.kind() == "parameter" {
4926            labels.push(swift_parameter_argument_label(current, source));
4927            continue;
4928        }
4929
4930        push_named_children_rev(&mut worklist, current);
4931    }
4932
4933    labels
4934}
4935
4936fn swift_parameter_argument_label(parameter: tree_sitter::Node, source: &[u8]) -> Option<String> {
4937    parameter
4938        .child_by_field_name("external_name")
4939        .or_else(|| parameter.child_by_field_name("name"))
4940        .and_then(|label| normalize_swift_label(label.utf8_text(source).ok()?))
4941}
4942
4943fn extract_swift_call_argument_labels(
4944    call: tree_sitter::Node,
4945    source: &[u8],
4946) -> Option<Vec<Option<String>>> {
4947    let mut cursor = call.walk();
4948    let call_suffix = call
4949        .named_children(&mut cursor)
4950        .find(|child| child.kind() == "call_suffix")?;
4951
4952    let mut suffix_cursor = call_suffix.walk();
4953    let value_arguments = call_suffix
4954        .named_children(&mut suffix_cursor)
4955        .find(|child| child.kind() == "value_arguments")?;
4956
4957    let mut labels = Vec::new();
4958    let mut arg_cursor = value_arguments.walk();
4959    for argument in value_arguments
4960        .named_children(&mut arg_cursor)
4961        .filter(|child| child.kind() == "value_argument")
4962    {
4963        let label = argument
4964            .child_by_field_name("name")
4965            .and_then(|label| normalize_swift_label(label.utf8_text(source).ok()?));
4966        labels.push(label);
4967    }
4968
4969    Some(labels)
4970}
4971
4972fn normalize_swift_label(label: &str) -> Option<String> {
4973    let label = label.trim().trim_end_matches(':').trim();
4974    if label.is_empty() || label == "_" {
4975        None
4976    } else {
4977        Some(label.to_string())
4978    }
4979}
4980
4981fn select_member_candidate(
4982    members: &[(String, String)],
4983    method: &str,
4984    argument_labels: Option<&[Option<String>]>,
4985    swift_call_signatures: &HashMap<String, SwiftCallSignature>,
4986) -> SwiftOverloadSelection {
4987    let candidates: Vec<&String> = members
4988        .iter()
4989        .filter_map(|(name, id)| (name == method).then_some(id))
4990        .collect();
4991
4992    if argument_labels.is_none()
4993        && has_ambiguous_swift_signature_candidates(&candidates, swift_call_signatures)
4994    {
4995        return SwiftOverloadSelection::NoMatch;
4996    }
4997
4998    match select_swift_overload_candidate(&candidates, argument_labels, swift_call_signatures) {
4999        SwiftOverloadSelection::NotApplicable => candidates
5000            .first()
5001            .map(|id| SwiftOverloadSelection::Matched((*id).clone()))
5002            .unwrap_or(SwiftOverloadSelection::NotApplicable),
5003        selection => selection,
5004    }
5005}
5006
5007fn has_ambiguous_swift_signature_candidates(
5008    candidates: &[&String],
5009    swift_call_signatures: &HashMap<String, SwiftCallSignature>,
5010) -> bool {
5011    candidates
5012        .iter()
5013        .filter(|candidate| swift_call_signatures.contains_key(candidate.as_str()))
5014        .take(2)
5015        .count()
5016        > 1
5017}
5018
5019fn select_swift_overload_candidate(
5020    candidates: &[&String],
5021    argument_labels: Option<&[Option<String>]>,
5022    swift_call_signatures: &HashMap<String, SwiftCallSignature>,
5023) -> SwiftOverloadSelection {
5024    let Some(argument_labels) = argument_labels else {
5025        return SwiftOverloadSelection::NotApplicable;
5026    };
5027
5028    let signature_candidates: Vec<(&String, &SwiftCallSignature)> = candidates
5029        .iter()
5030        .copied()
5031        .filter_map(|candidate| {
5032            swift_call_signatures
5033                .get(candidate.as_str())
5034                .map(|signature| (candidate, signature))
5035        })
5036        .collect();
5037    if signature_candidates.is_empty() {
5038        return SwiftOverloadSelection::NotApplicable;
5039    }
5040
5041    let exact_matches: Vec<&String> = signature_candidates
5042        .iter()
5043        .filter_map(|(candidate, signature)| {
5044            (signature.argument_labels.as_slice() == argument_labels).then_some(*candidate)
5045        })
5046        .collect();
5047    if exact_matches.len() == 1 {
5048        return SwiftOverloadSelection::Matched(exact_matches[0].clone());
5049    }
5050    if exact_matches.len() > 1 {
5051        return SwiftOverloadSelection::NoMatch;
5052    }
5053
5054    if argument_labels.iter().all(Option::is_none) {
5055        let same_arity_matches: Vec<&String> = signature_candidates
5056            .iter()
5057            .filter_map(|(candidate, signature)| {
5058                (signature.argument_labels.len() == argument_labels.len()).then_some(*candidate)
5059            })
5060            .collect();
5061        if same_arity_matches.len() == 1 {
5062            return SwiftOverloadSelection::Matched(same_arity_matches[0].clone());
5063        }
5064        if same_arity_matches.len() > 1 {
5065            return SwiftOverloadSelection::NoMatch;
5066        }
5067    }
5068
5069    SwiftOverloadSelection::NoMatch
5070}
5071
5072/// Collect ALL AST references in a file with a single tree walk.
5073/// Each ref records its row so callers can bucket refs into entities by line range.
5074fn collect_all_file_refs(
5075    root: tree_sitter::Node,
5076    source: &[u8],
5077    config: &ScopeResolveConfig,
5078) -> Vec<AstRef> {
5079    let mut refs = Vec::new();
5080    let mut worklist = vec![root];
5081    while let Some(node) = worklist.pop() {
5082        let node_row = node.start_position().row;
5083        let kind = node.kind();
5084
5085        // Call nodes (e.g. "call", "call_expression", "method_invocation")
5086        if config.call_nodes.contains(&kind) {
5087            match &config.call_style {
5088                CallNodeStyle::FunctionField(field) => {
5089                    if let Some(func) = node.child_by_field_name(field) {
5090                        // Pass empty entity_name — self-ref filtering is done at resolution time
5091                        extract_call_ref(
5092                            func, node, "", "", source, &mut refs, config, node_row, None,
5093                        );
5094                    }
5095                }
5096                CallNodeStyle::FirstChild => {
5097                    // Swift/Kotlin: callee is the first named child (identifier or navigation_expression)
5098                    if let Some(func) = node.named_child(0) {
5099                        let argument_labels = extract_swift_call_argument_labels(node, source);
5100                        extract_call_ref(
5101                            func,
5102                            node,
5103                            "",
5104                            "",
5105                            source,
5106                            &mut refs,
5107                            config,
5108                            node_row,
5109                            argument_labels,
5110                        );
5111                    }
5112                }
5113                CallNodeStyle::DirectMethod {
5114                    object_field,
5115                    method_field,
5116                } => {
5117                    let method_name = node
5118                        .child_by_field_name(method_field)
5119                        .and_then(|n| n.utf8_text(source).ok())
5120                        .unwrap_or("");
5121                    if !method_name.is_empty() && !is_builtin(method_name, config) {
5122                        if let Some(obj_node) = node.child_by_field_name(object_field) {
5123                            let receiver = obj_node.utf8_text(source).unwrap_or("").to_string();
5124                            let receiver = receiver.trim_end_matches('.').to_string();
5125                            refs.push(AstRef {
5126                                kind: AstRefKind::MethodCall {
5127                                    receiver,
5128                                    method: method_name.to_string(),
5129                                    argument_labels: None,
5130                                },
5131                                row: node_row,
5132                                start_byte: node.start_byte(),
5133                                end_byte: node.end_byte(),
5134                            });
5135                        } else {
5136                            refs.push(AstRef {
5137                                kind: AstRefKind::Call {
5138                                    name: method_name.to_string(),
5139                                    argument_labels: None,
5140                                },
5141                                row: node_row,
5142                                start_byte: node.start_byte(),
5143                                end_byte: node.end_byte(),
5144                            });
5145                        }
5146                    }
5147                }
5148            }
5149            push_named_children_rev(&mut worklist, node);
5150            continue;
5151        }
5152
5153        // Macro invocations (Rust: macro_invocation, macro name in "macro" field)
5154        if kind == "macro_invocation" {
5155            if let Some(macro_node) = node.child_by_field_name("macro") {
5156                let macro_name = macro_node.utf8_text(source).unwrap_or("");
5157                if !macro_name.is_empty() && !is_builtin(macro_name, config) {
5158                    refs.push(AstRef {
5159                        kind: AstRefKind::Call {
5160                            name: macro_name.to_string(),
5161                            argument_labels: None,
5162                        },
5163                        row: macro_node.start_position().row,
5164                        start_byte: macro_node.start_byte(),
5165                        end_byte: macro_node.end_byte(),
5166                    });
5167                }
5168            }
5169            push_named_children_rev(&mut worklist, node);
5170            continue;
5171        }
5172
5173        // New expression nodes (e.g. "new_expression", "object_creation_expression")
5174        if config.new_expr_nodes.contains(&kind) {
5175            if let Some(type_node) = node.child_by_field_name(config.new_expr_type_field) {
5176                let name = type_node.utf8_text(source).unwrap_or("");
5177                let name = name.rsplit('.').next().unwrap_or(name);
5178                if !name.is_empty() && !is_builtin(name, config) {
5179                    refs.push(AstRef {
5180                        kind: AstRefKind::Call {
5181                            name: name.to_string(),
5182                            argument_labels: None,
5183                        },
5184                        row: type_node.start_position().row,
5185                        start_byte: type_node.start_byte(),
5186                        end_byte: type_node.end_byte(),
5187                    });
5188                }
5189            }
5190            push_named_children_rev(&mut worklist, node);
5191            continue;
5192        }
5193
5194        // Composite literal nodes (e.g. Go "composite_literal")
5195        if config.composite_literal_nodes.contains(&kind) {
5196            if let Some(type_node) = node.child_by_field_name("type") {
5197                let name = type_node.utf8_text(source).unwrap_or("");
5198                if name.chars().next().map_or(false, |c| c.is_uppercase())
5199                    && !is_builtin(name, config)
5200                {
5201                    refs.push(AstRef {
5202                        kind: AstRefKind::Call {
5203                            name: name.to_string(),
5204                            argument_labels: None,
5205                        },
5206                        row: type_node.start_position().row,
5207                        start_byte: type_node.start_byte(),
5208                        end_byte: type_node.end_byte(),
5209                    });
5210                }
5211            }
5212        }
5213
5214        // Recurse into children
5215        push_named_children_rev(&mut worklist, node);
5216    }
5217    refs
5218}
5219
5220fn build_refs_by_row(refs: &[AstRef]) -> Vec<Vec<usize>> {
5221    let max_row = refs.iter().map(|r| r.row).max().unwrap_or(0);
5222    let mut refs_by_row = vec![Vec::new(); max_row + 1];
5223    for (idx, ast_ref) in refs.iter().enumerate() {
5224        refs_by_row[ast_ref.row].push(idx);
5225    }
5226    refs_by_row
5227}
5228
5229/// Extract a call reference from a function/callee node (shared across languages)
5230fn extract_call_ref(
5231    func: tree_sitter::Node,
5232    ref_node: tree_sitter::Node,
5233    _entity_id: &str,
5234    entity_name: &str,
5235    source: &[u8],
5236    refs: &mut Vec<AstRef>,
5237    config: &ScopeResolveConfig,
5238    row: usize,
5239    argument_labels: Option<Vec<Option<String>>>,
5240) {
5241    let func_kind = func.kind();
5242
5243    if func_kind == "identifier"
5244        || func_kind == "simple_identifier"
5245        || func_kind == "type_identifier"
5246    {
5247        let name = func.utf8_text(source).unwrap_or("");
5248        if !name.is_empty() && name != entity_name && !is_builtin(name, config) {
5249            refs.push(AstRef {
5250                kind: AstRefKind::Call {
5251                    name: name.to_string(),
5252                    argument_labels,
5253                },
5254                row,
5255                start_byte: ref_node.start_byte(),
5256                end_byte: ref_node.end_byte(),
5257            });
5258        }
5259        return;
5260    }
5261
5262    // Check config member_access patterns
5263    for ma in config.member_access {
5264        if func_kind == ma.node_kind {
5265            extract_member_call_ref(
5266                func,
5267                ref_node,
5268                ma.object_field,
5269                ma.property_field,
5270                source,
5271                refs,
5272                row,
5273                argument_labels,
5274            );
5275            return;
5276        }
5277    }
5278
5279    // Scoped call nodes (e.g. Rust "scoped_identifier" for Type::method)
5280    if config.scoped_call_nodes.contains(&func_kind) {
5281        let text = func.utf8_text(source).unwrap_or("");
5282        let parts: Vec<&str> = text.split("::").collect();
5283        if parts.len() >= 2 {
5284            // Handle super:: and self:: prefixed paths by emitting a call
5285            // to the final name so scope chain resolution can find it.
5286            let is_path_prefix = parts[0] == "super" || parts[0] == "self" || parts[0] == "crate";
5287            let method_name = parts[parts.len() - 1];
5288            if is_path_prefix && parts.len() == 2 {
5289                if !method_name.is_empty() && !is_builtin(method_name, config) {
5290                    refs.push(AstRef {
5291                        kind: AstRefKind::Call {
5292                            name: method_name.to_string(),
5293                            argument_labels: None,
5294                        },
5295                        row,
5296                        start_byte: ref_node.start_byte(),
5297                        end_byte: ref_node.end_byte(),
5298                    });
5299                }
5300            } else {
5301                let receiver = parts[..parts.len() - 1].join("::");
5302                let receiver_base = parts[parts.len() - 2];
5303                if !receiver.is_empty() && !method_name.is_empty() {
5304                    if parts.len() == 2
5305                        && receiver_base
5306                            .chars()
5307                            .next()
5308                            .map_or(false, |c| c.is_uppercase())
5309                        && !is_builtin(receiver_base, config)
5310                    {
5311                        refs.push(AstRef {
5312                            kind: AstRefKind::MethodCall {
5313                                receiver: receiver_base.to_string(),
5314                                method: method_name.to_string(),
5315                                argument_labels: None,
5316                            },
5317                            row,
5318                            start_byte: ref_node.start_byte(),
5319                            end_byte: ref_node.end_byte(),
5320                        });
5321                    } else if !is_builtin(method_name, config) {
5322                        refs.push(AstRef {
5323                            kind: AstRefKind::ScopedCall {
5324                                path: receiver,
5325                                name: method_name.to_string(),
5326                            },
5327                            row,
5328                            start_byte: ref_node.start_byte(),
5329                            end_byte: ref_node.end_byte(),
5330                        });
5331                    }
5332                }
5333            }
5334        }
5335    }
5336}
5337
5338/// Extract a member/method call from a node with object+property fields.
5339/// Falls back to positional children for languages like Kotlin where
5340/// navigation_expression children don't have field names.
5341fn extract_member_call_ref(
5342    node: tree_sitter::Node,
5343    ref_node: tree_sitter::Node,
5344    object_field: &str,
5345    attr_field: &str,
5346    source: &[u8],
5347    refs: &mut Vec<AstRef>,
5348    row: usize,
5349    argument_labels: Option<Vec<Option<String>>>,
5350) {
5351    let obj_text = node
5352        .child_by_field_name(object_field)
5353        .and_then(|n| n.utf8_text(source).ok())
5354        .unwrap_or("");
5355
5356    let attr_text = node
5357        .child_by_field_name(attr_field)
5358        .and_then(|n| {
5359            let text = n.utf8_text(source).ok()?;
5360            // Swift navigation_suffix includes the dot prefix (.validate → validate)
5361            Some(text.trim_start_matches('.'))
5362        })
5363        .unwrap_or("");
5364
5365    if !obj_text.is_empty() && !attr_text.is_empty() {
5366        push_method_call_ref(obj_text, attr_text, refs, ref_node, row, argument_labels);
5367        return;
5368    }
5369
5370    // Fallback: positional children (Kotlin navigation_expression has no field names)
5371    let child_count = node.named_child_count();
5372    if child_count >= 2 {
5373        let obj = node
5374            .named_child(0)
5375            .and_then(|n| n.utf8_text(source).ok())
5376            .unwrap_or("");
5377        let last_idx = (child_count - 1) as u32;
5378        let attr = node
5379            .named_child(last_idx)
5380            .and_then(|n| n.utf8_text(source).ok())
5381            .unwrap_or("");
5382        if !obj.is_empty() && !attr.is_empty() {
5383            push_method_call_ref(obj, attr, refs, ref_node, row, argument_labels);
5384        }
5385    }
5386}
5387
5388fn push_method_call_ref(
5389    obj: &str,
5390    method: &str,
5391    refs: &mut Vec<AstRef>,
5392    node: tree_sitter::Node,
5393    row: usize,
5394    argument_labels: Option<Vec<Option<String>>>,
5395) {
5396    refs.push(AstRef {
5397        kind: AstRefKind::MethodCall {
5398            receiver: obj.to_string(),
5399            method: method.to_string(),
5400            argument_labels,
5401        },
5402        row,
5403        start_byte: node.start_byte(),
5404        end_byte: node.end_byte(),
5405    });
5406}
5407
5408/// Resolve a single reference against scopes and symbol tables.
5409fn resolve_ref(
5410    ast_ref: &AstRef,
5411    scope_idx: usize,
5412    scopes: &[Scope],
5413    symbol_table: &HashMap<String, Vec<String>>,
5414    class_members: &HashMap<String, Vec<(String, String)>>,
5415    owner_members: &HashMap<String, Vec<(String, String)>>,
5416    import_table_by_name: &HashMap<&str, &str>,
5417    instance_attr_types: &HashMap<(String, String), String>,
5418    entity_map: &HashMap<String, EntityInfo>,
5419    swift_call_signatures: &HashMap<String, SwiftCallSignature>,
5420    file_path: &str,
5421    from_entity_id: &str,
5422    allow_cross_file_calls: bool,
5423    allow_implicit_instance_member_receiver: bool,
5424    file_lookup: &FileEntityLookup<'_>,
5425    lookup_cache: &mut ScopeLookupCache,
5426) -> Option<(String, RefType, &'static str)> {
5427    match &ast_ref.kind {
5428        AstRefKind::Call {
5429            name,
5430            argument_labels,
5431        } => {
5432            if is_local_binding_in_scopes_cached(scope_idx, scopes, name, lookup_cache) {
5433                return None;
5434            }
5435
5436            // Swift overload disambiguation needs call-signature data that is only
5437            // built for Swift sources. For every other language the pre-resolution
5438            // candidate scan below is inert, yet it scans the global symbol table —
5439            // in a large monorepo a single common name maps to thousands of entities,
5440            // so this scan dominates graph resolution. Skip it unless Swift
5441            // signatures are present.
5442            if !swift_call_signatures.is_empty() {
5443                if argument_labels.is_some() {
5444                    if let Some(target_ids) = symbol_table.get(name.as_str()) {
5445                        let same_file_targets: Vec<&String> = target_ids
5446                            .iter()
5447                            .filter(|id| {
5448                                entity_map
5449                                    .get(*id)
5450                                    .map_or(false, |e| e.file_path == file_path)
5451                            })
5452                            .collect();
5453                        let visible_targets: Vec<&String> = if !same_file_targets.is_empty() {
5454                            same_file_targets
5455                        } else if allow_cross_file_calls {
5456                            target_ids.iter().collect()
5457                        } else {
5458                            Vec::new()
5459                        };
5460                        match select_swift_overload_candidate(
5461                            &visible_targets,
5462                            argument_labels.as_deref(),
5463                            swift_call_signatures,
5464                        ) {
5465                            SwiftOverloadSelection::Matched(target_id) => {
5466                                let is_constructor =
5467                                    name.chars().next().map_or(false, |c| c.is_uppercase());
5468                                let ref_type = if is_constructor {
5469                                    RefType::TypeRef
5470                                } else {
5471                                    RefType::Calls
5472                                };
5473                                return Some((target_id, ref_type, "scope_chain"));
5474                            }
5475                            SwiftOverloadSelection::NoMatch => return None,
5476                            SwiftOverloadSelection::NotApplicable => {}
5477                        }
5478                    }
5479                } else if let Some(target_ids) = symbol_table.get(name.as_str()) {
5480                    let same_file_targets: Vec<&String> = target_ids
5481                        .iter()
5482                        .filter(|id| {
5483                            entity_map
5484                                .get(*id)
5485                                .map_or(false, |e| e.file_path == file_path)
5486                        })
5487                        .collect();
5488                    let visible_targets: Vec<&String> = if !same_file_targets.is_empty() {
5489                        same_file_targets
5490                    } else if allow_cross_file_calls {
5491                        target_ids.iter().collect()
5492                    } else {
5493                        Vec::new()
5494                    };
5495                    if has_ambiguous_swift_signature_candidates(
5496                        &visible_targets,
5497                        swift_call_signatures,
5498                    ) {
5499                        return None;
5500                    }
5501                }
5502            }
5503
5504            // 1. Walk scope chain for the name
5505            if let Some(eid) = lookup_scope_chain_cached(scope_idx, scopes, name, lookup_cache) {
5506                if eid != from_entity_id {
5507                    return Some((eid, RefType::Calls, "scope_chain"));
5508                }
5509            }
5510
5511            // 2. Check import table. The per-file table only holds this file's
5512            // imports, so a name lookup suffices — avoiding a (path, name) key string
5513            // allocated for every reference (millions on a large repo).
5514            if let Some(target_id) = import_table_by_name.get(name.as_str()) {
5515                return Some(((*target_id).to_string(), RefType::Calls, "import"));
5516            }
5517
5518            // 3. Global symbol table fallback (constructor calls or cross-file functions)
5519            if let Some(target_ids) = symbol_table.get(name.as_str()) {
5520                let is_constructor = name.chars().next().map_or(false, |c| c.is_uppercase());
5521                let ref_type = if is_constructor {
5522                    RefType::TypeRef
5523                } else {
5524                    RefType::Calls
5525                };
5526
5527                if swift_call_signatures.is_empty() {
5528                    // Fast path: the per-file name index gives the first same-file
5529                    // definition in O(1); the cross-file fallback takes the first
5530                    // global definition. Both preserve entity-discovery order, so the
5531                    // result matches the candidate scan below without iterating the
5532                    // thousands of same-named entities a monorepo accumulates.
5533                    let target = file_lookup
5534                        .first_id_by_name(name)
5535                        .map(str::to_string)
5536                        .or_else(|| {
5537                            if is_constructor || allow_cross_file_calls {
5538                                target_ids.first().cloned()
5539                            } else {
5540                                None
5541                            }
5542                        });
5543                    if let Some(tid) = target {
5544                        return Some((tid, ref_type, "scope_chain"));
5545                    }
5546                    return None;
5547                }
5548
5549                let same_file_targets: Vec<&String> = target_ids
5550                    .iter()
5551                    .filter(|id| {
5552                        entity_map
5553                            .get(*id)
5554                            .map_or(false, |e| e.file_path == file_path)
5555                    })
5556                    .collect();
5557                let visible_targets: Vec<&String> = if !same_file_targets.is_empty() {
5558                    same_file_targets
5559                } else if is_constructor || allow_cross_file_calls {
5560                    target_ids.iter().collect()
5561                } else {
5562                    Vec::new()
5563                };
5564                let target = match select_swift_overload_candidate(
5565                    &visible_targets,
5566                    argument_labels.as_deref(),
5567                    swift_call_signatures,
5568                ) {
5569                    SwiftOverloadSelection::Matched(target_id) => Some(target_id),
5570                    SwiftOverloadSelection::NoMatch => return None,
5571                    SwiftOverloadSelection::NotApplicable => {
5572                        visible_targets.first().map(|id| (*id).clone())
5573                    }
5574                };
5575                if let Some(tid) = target {
5576                    return Some((tid, ref_type, "scope_chain"));
5577                }
5578            }
5579
5580            None
5581        }
5582
5583        AstRefKind::ScopedCall { .. } => None,
5584
5585        AstRefKind::MethodCall {
5586            receiver: raw_receiver,
5587            method,
5588            argument_labels,
5589        } => {
5590            // Strip prefix operators like ! (Swift: `!dog.validate()`)
5591            let receiver = raw_receiver.trim_start_matches('!').trim_start_matches('~');
5592            if receiver == "self" || receiver == "this" {
5593                // self.method() -> find in enclosing class
5594                let mut idx = scope_idx;
5595                loop {
5596                    if scopes[idx].kind == "class" {
5597                        if let Some(class_name) = scopes[idx]
5598                            .owner_id
5599                            .as_ref()
5600                            .and_then(|owner_id| entity_map.get(owner_id))
5601                            .map(|owner| owner.name.as_str())
5602                        {
5603                            if let Some(members) = class_members.get(class_name) {
5604                                match select_member_candidate(
5605                                    members,
5606                                    method,
5607                                    argument_labels.as_deref(),
5608                                    swift_call_signatures,
5609                                ) {
5610                                    SwiftOverloadSelection::Matched(eid) => {
5611                                        return Some((eid, RefType::Calls, "scope_chain"));
5612                                    }
5613                                    SwiftOverloadSelection::NoMatch => return None,
5614                                    SwiftOverloadSelection::NotApplicable => {
5615                                        if argument_labels.is_some() {
5616                                            return None;
5617                                        }
5618                                    }
5619                                }
5620                            }
5621                        }
5622                        if let Some(eid) = scopes[idx].defs.get(method.as_str()) {
5623                            return Some((eid.clone(), RefType::Calls, "scope_chain"));
5624                        }
5625                        break;
5626                    }
5627                    match scopes[idx].parent {
5628                        Some(p) => idx = p,
5629                        None => break,
5630                    }
5631                }
5632                return None;
5633            }
5634
5635            // Handle chained self.attr.method() pattern
5636            // receiver is "self.X" where X is an instance attribute
5637            if receiver.starts_with("self.") || receiver.starts_with("this.") {
5638                let attr_name = &receiver[5..]; // strip "self." or "this."
5639                                                // Find the enclosing class name
5640                let class_name =
5641                    find_enclosing_class_cached(scope_idx, scopes, entity_map, lookup_cache);
5642                if let Some(cn) = class_name {
5643                    // Look up instance attribute type
5644                    if let Some(attr_type) = instance_attr_types.get(&(cn, attr_name.to_string())) {
5645                        if let Some(members) = class_members.get(attr_type.as_str()) {
5646                            match select_member_candidate(
5647                                members,
5648                                method,
5649                                argument_labels.as_deref(),
5650                                swift_call_signatures,
5651                            ) {
5652                                SwiftOverloadSelection::Matched(mid) => {
5653                                    return Some((mid, RefType::Calls, "type_tracking"));
5654                                }
5655                                SwiftOverloadSelection::NoMatch => return None,
5656                                SwiftOverloadSelection::NotApplicable => {}
5657                            }
5658                        }
5659                    }
5660                }
5661            }
5662
5663            // Handle chained var.field.method() pattern (e.g. Go receiver: t.Conn.Execute())
5664            if receiver.contains('.')
5665                && !receiver.starts_with("self.")
5666                && !receiver.starts_with("this.")
5667            {
5668                if let Some(dot_pos) = receiver.find('.') {
5669                    let var_part = &receiver[..dot_pos];
5670                    let field_part = &receiver[dot_pos + 1..];
5671                    if let Some(var_type) =
5672                        lookup_type_in_scopes_cached(scope_idx, scopes, var_part, lookup_cache)
5673                    {
5674                        if let Some(attr_type) =
5675                            instance_attr_types.get(&(var_type, field_part.to_string()))
5676                        {
5677                            if let Some(members) = class_members.get(attr_type.as_str()) {
5678                                match select_member_candidate(
5679                                    members,
5680                                    method,
5681                                    argument_labels.as_deref(),
5682                                    swift_call_signatures,
5683                                ) {
5684                                    SwiftOverloadSelection::Matched(mid) => {
5685                                        return Some((mid, RefType::Calls, "type_tracking"));
5686                                    }
5687                                    SwiftOverloadSelection::NoMatch => return None,
5688                                    SwiftOverloadSelection::NotApplicable => {}
5689                                }
5690                            }
5691                        }
5692                    }
5693                }
5694            }
5695
5696            // receiver.method() -> look up receiver type, then resolve method
5697            let receiver_type = if let Some(receiver_type) =
5698                lookup_type_before_class_scope(scope_idx, scopes, receiver)
5699            {
5700                Some(receiver_type)
5701            } else if allow_implicit_instance_member_receiver
5702                && is_simple_identifier_name(receiver)
5703                && !is_local_binding_in_scopes_cached(scope_idx, scopes, receiver, lookup_cache)
5704            {
5705                match find_enclosing_class_cached(scope_idx, scopes, entity_map, lookup_cache) {
5706                    Some(class_name) => instance_attr_types
5707                        .get(&(class_name, receiver.to_string()))
5708                        .cloned(),
5709                    None => None,
5710                }
5711            } else {
5712                None
5713            };
5714
5715            if let Some(class_name) = receiver_type {
5716                if let Some(members) = class_members.get(class_name.as_str()) {
5717                    match select_member_candidate(
5718                        members,
5719                        method,
5720                        argument_labels.as_deref(),
5721                        swift_call_signatures,
5722                    ) {
5723                        SwiftOverloadSelection::Matched(mid) => {
5724                            return Some((mid, RefType::Calls, "type_tracking"));
5725                        }
5726                        SwiftOverloadSelection::NoMatch => return None,
5727                        SwiftOverloadSelection::NotApplicable => {}
5728                    }
5729                }
5730            }
5731
5732            // Inside class methods, unqualified property receivers resolve
5733            // against the enclosing instance when no local binding shadows them.
5734            let from_entity_is_container_type =
5735                entity_map.get(from_entity_id).map_or(false, |entity| {
5736                    matches!(
5737                        entity.entity_type.as_str(),
5738                        "class"
5739                            | "struct"
5740                            | "interface"
5741                            | "enum"
5742                            | "protocol_declaration"
5743                            | "object_declaration"
5744                            | "companion_object"
5745                    )
5746                });
5747
5748            if allow_implicit_instance_member_receiver
5749                && !from_entity_is_container_type
5750                && !is_local_binding_in_scopes_cached(scope_idx, scopes, receiver, lookup_cache)
5751            {
5752                if let Some(class_name) =
5753                    find_enclosing_class_cached(scope_idx, scopes, entity_map, lookup_cache)
5754                {
5755                    if let Some(attr_type) =
5756                        instance_attr_types.get(&(class_name, receiver.to_string()))
5757                    {
5758                        if let Some(members) = class_members.get(attr_type.as_str()) {
5759                            match select_member_candidate(
5760                                members,
5761                                method,
5762                                argument_labels.as_deref(),
5763                                swift_call_signatures,
5764                            ) {
5765                                SwiftOverloadSelection::Matched(mid) => {
5766                                    return Some((mid, RefType::Calls, "type_tracking"));
5767                                }
5768                                SwiftOverloadSelection::NoMatch => return None,
5769                                SwiftOverloadSelection::NotApplicable => {}
5770                            }
5771                        }
5772                    }
5773                }
5774            }
5775
5776            // ClassName.method() static call, only when ClassName is visible and
5777            // not shadowed by a local binding.
5778            if !is_local_binding_in_scopes_cached(scope_idx, scopes, receiver, lookup_cache) {
5779                if let Some(class_id) =
5780                    lookup_scope_chain_cached(scope_idx, scopes, receiver, lookup_cache)
5781                {
5782                    if let Some(info) = entity_map.get(&class_id) {
5783                        if matches!(info.entity_type.as_str(), "module" | "variable" | "object")
5784                            && info.name == receiver
5785                        {
5786                            if let Some(mid) =
5787                                lookup_entity_member(owner_members, &class_id, method).or_else(
5788                                    || lookup_owned_scope_member(scopes, &class_id, method),
5789                                )
5790                            {
5791                                return Some((mid, RefType::Calls, "scope_chain"));
5792                            }
5793                        } else if matches!(
5794                            info.entity_type.as_str(),
5795                            "class" | "struct" | "interface"
5796                        ) && info.name == receiver
5797                        {
5798                            if let Some(members) = class_members.get(&info.name) {
5799                                match select_member_candidate(
5800                                    members,
5801                                    method,
5802                                    argument_labels.as_deref(),
5803                                    swift_call_signatures,
5804                                ) {
5805                                    SwiftOverloadSelection::Matched(mid) => {
5806                                        return Some((mid, RefType::Calls, "scope_chain"));
5807                                    }
5808                                    SwiftOverloadSelection::NoMatch => return None,
5809                                    SwiftOverloadSelection::NotApplicable => {}
5810                                }
5811                            }
5812                        }
5813                    }
5814                }
5815            }
5816
5817            // Fallback: check import table for the receiver
5818            if !is_local_binding_in_scopes_cached(scope_idx, scopes, receiver, lookup_cache) {
5819                if let Some(target_id) = import_table_by_name.get(receiver) {
5820                    if let Some(info) = entity_map.get(*target_id) {
5821                        if matches!(info.entity_type.as_str(), "class" | "struct") {
5822                            if let Some(members) = class_members.get(&info.name) {
5823                                match select_member_candidate(
5824                                    members,
5825                                    method,
5826                                    argument_labels.as_deref(),
5827                                    swift_call_signatures,
5828                                ) {
5829                                    SwiftOverloadSelection::Matched(mid) => {
5830                                        return Some((mid, RefType::Calls, "type_tracking"));
5831                                    }
5832                                    SwiftOverloadSelection::NoMatch => return None,
5833                                    SwiftOverloadSelection::NotApplicable => {}
5834                                }
5835                            }
5836                        }
5837                    }
5838                }
5839
5840                // Namespace import: alias.method()
5841                let namespaced = format!("{receiver}.{method}");
5842                if let Some(target_id) = import_table_by_name.get(namespaced.as_str()) {
5843                    return Some(((*target_id).to_string(), RefType::Calls, "import"));
5844                }
5845            }
5846
5847            // Go package-qualified call: package.Function()
5848            // Try the method name directly in the import table
5849            if file_path.ends_with(".go") {
5850                if let Some(target_id) = import_table_by_name.get(method.as_str()) {
5851                    return Some(((*target_id).to_string(), RefType::Calls, "import"));
5852                }
5853            }
5854
5855            None
5856        }
5857    }
5858}
5859
5860fn allows_implicit_instance_member_receiver(
5861    file_path: &str,
5862    entity_type: &str,
5863    entity_content: &str,
5864) -> bool {
5865    let ext = file_path.rsplit('.').next().unwrap_or("");
5866    let supports_implicit_receiver = matches!(
5867        ext,
5868        "swift"
5869            | "kt"
5870            | "kts"
5871            | "java"
5872            | "cs"
5873            | "cpp"
5874            | "cc"
5875            | "cxx"
5876            | "hpp"
5877            | "hh"
5878            | "hxx"
5879            | "h"
5880            | "scala"
5881            | "dart"
5882    );
5883
5884    supports_implicit_receiver
5885        && matches!(
5886            entity_type,
5887            "function" | "method" | "init" | "init_declaration" | "constructor_declaration"
5888        )
5889        && !has_static_member_modifier(ext, entity_content)
5890}
5891
5892fn has_static_member_modifier(ext: &str, entity_content: &str) -> bool {
5893    let header = entity_content
5894        .split(|ch| ch == '{' || ch == '=')
5895        .next()
5896        .unwrap_or(entity_content);
5897    let header_without_comments = strip_member_header_comments(header);
5898    let tokens = header_without_comments
5899        .split(|ch: char| !ch.is_alphanumeric() && ch != '_')
5900        .filter(|token| !token.is_empty())
5901        .collect::<Vec<_>>();
5902    let declaration_start = tokens
5903        .iter()
5904        .position(|token| {
5905            matches!(
5906                *token,
5907                "func" | "function" | "fn" | "constructor" | "init" | "var" | "let" | "subscript"
5908            )
5909        })
5910        .unwrap_or(tokens.len());
5911
5912    tokens[..declaration_start]
5913        .iter()
5914        .any(|token| *token == "static" || (ext == "swift" && *token == "class"))
5915}
5916
5917fn strip_member_header_comments(header: &str) -> String {
5918    let mut output = String::with_capacity(header.len());
5919    let mut chars = header.chars().peekable();
5920
5921    while let Some(ch) = chars.next() {
5922        if ch != '/' {
5923            output.push(ch);
5924            continue;
5925        }
5926
5927        match chars.peek().copied() {
5928            Some('/') => {
5929                chars.next();
5930                for next in chars.by_ref() {
5931                    if next == '\n' {
5932                        output.push(' ');
5933                        break;
5934                    }
5935                }
5936            }
5937            Some('*') => {
5938                chars.next();
5939                let mut previous = '\0';
5940                for next in chars.by_ref() {
5941                    if previous == '*' && next == '/' {
5942                        break;
5943                    }
5944                    previous = next;
5945                }
5946                output.push(' ');
5947            }
5948            _ => output.push(ch),
5949        }
5950    }
5951
5952    output
5953}
5954
5955fn is_simple_identifier_name(name: &str) -> bool {
5956    let mut chars = name.chars();
5957    let Some(first) = chars.next() else {
5958        return false;
5959    };
5960
5961    (first == '_' || first.is_alphabetic()) && chars.all(|ch| ch == '_' || ch.is_alphanumeric())
5962}
5963
5964fn lookup_owned_scope_member(scopes: &[Scope], owner_id: &str, member: &str) -> Option<String> {
5965    scopes
5966        .iter()
5967        .find(|scope| scope.owner_id.as_deref() == Some(owner_id))
5968        .and_then(|scope| scope.defs.get(member).cloned())
5969}
5970
5971fn lookup_entity_member(
5972    owner_members: &HashMap<String, Vec<(String, String)>>,
5973    owner_id: &str,
5974    member: &str,
5975) -> Option<String> {
5976    owner_members
5977        .get(owner_id)
5978        .and_then(|members| members.iter().find(|(name, _)| name == member))
5979        .map(|(_, id)| id.clone())
5980}
5981
5982/// Find the class name for the enclosing class scope.
5983fn find_enclosing_class(
5984    start_scope: usize,
5985    scopes: &[Scope],
5986    entity_map: &HashMap<String, EntityInfo>,
5987) -> Option<String> {
5988    let mut idx = start_scope;
5989    loop {
5990        if scopes[idx].kind == "class" {
5991            if let Some(ref oid) = scopes[idx].owner_id {
5992                return entity_map.get(oid).map(|e| e.name.clone());
5993            }
5994        }
5995        match scopes[idx].parent {
5996            Some(p) => idx = p,
5997            None => return None,
5998        }
5999    }
6000}
6001
6002fn find_enclosing_class_cached(
6003    start_scope: usize,
6004    scopes: &[Scope],
6005    entity_map: &HashMap<String, EntityInfo>,
6006    cache: &mut ScopeLookupCache,
6007) -> Option<String> {
6008    if let Some(cached) = cache.enclosing_classes.get(&start_scope) {
6009        return cached.clone();
6010    }
6011    let value = find_enclosing_class(start_scope, scopes, entity_map);
6012    cache.enclosing_classes.insert(start_scope, value.clone());
6013    value
6014}
6015
6016/// Walk up the scope chain looking for a definition.
6017fn lookup_scope_chain(start_scope: usize, scopes: &[Scope], name: &str) -> Option<String> {
6018    let mut idx = start_scope;
6019    loop {
6020        if let Some(eid) = scopes[idx].defs.get(name) {
6021            return Some(eid.clone());
6022        }
6023        match scopes[idx].parent {
6024            Some(p) => idx = p,
6025            None => return None,
6026        }
6027    }
6028}
6029
6030fn lookup_scope_chain_cached(
6031    start_scope: usize,
6032    scopes: &[Scope],
6033    name: &str,
6034    cache: &mut ScopeLookupCache,
6035) -> Option<String> {
6036    if let Some(cached) = cache
6037        .defs
6038        .get(&start_scope)
6039        .and_then(|scope_cache| scope_cache.get(name))
6040    {
6041        return cached.clone();
6042    }
6043    let value = lookup_scope_chain(start_scope, scopes, name);
6044    cache
6045        .defs
6046        .entry(start_scope)
6047        .or_default()
6048        .insert(name.to_string(), value.clone());
6049    value
6050}
6051
6052/// Walk up the scope chain looking for a local binding that shadows a definition.
6053fn is_local_binding_in_scopes(start_scope: usize, scopes: &[Scope], name: &str) -> bool {
6054    let mut idx = start_scope;
6055    loop {
6056        if scopes[idx].bindings.contains(name) {
6057            return true;
6058        }
6059        match scopes[idx].parent {
6060            Some(p) => idx = p,
6061            None => return false,
6062        }
6063    }
6064}
6065
6066fn is_local_binding_in_scopes_cached(
6067    start_scope: usize,
6068    scopes: &[Scope],
6069    name: &str,
6070    cache: &mut ScopeLookupCache,
6071) -> bool {
6072    if let Some(cached) = cache
6073        .local_bindings
6074        .get(&start_scope)
6075        .and_then(|scope_cache| scope_cache.get(name))
6076    {
6077        return *cached;
6078    }
6079    let value = is_local_binding_in_scopes(start_scope, scopes, name);
6080    cache
6081        .local_bindings
6082        .entry(start_scope)
6083        .or_default()
6084        .insert(name.to_string(), value);
6085    value
6086}
6087
6088/// Walk up the scope chain looking for a type binding.
6089fn lookup_type_in_scopes(start_scope: usize, scopes: &[Scope], var_name: &str) -> Option<String> {
6090    let mut idx = start_scope;
6091    loop {
6092        if let Some(type_name) = scopes[idx].types.get(var_name) {
6093            return Some(type_name.clone());
6094        }
6095        match scopes[idx].parent {
6096            Some(p) => idx = p,
6097            None => return None,
6098        }
6099    }
6100}
6101
6102fn lookup_type_before_class_scope(
6103    start_scope: usize,
6104    scopes: &[Scope],
6105    var_name: &str,
6106) -> Option<String> {
6107    let mut idx = start_scope;
6108    loop {
6109        if scopes[idx].kind == "class" {
6110            return None;
6111        }
6112        if let Some(type_name) = scopes[idx].types.get(var_name) {
6113            return Some(type_name.clone());
6114        }
6115        match scopes[idx].parent {
6116            Some(p) => idx = p,
6117            None => return None,
6118        }
6119    }
6120}
6121
6122fn lookup_type_in_scopes_cached(
6123    start_scope: usize,
6124    scopes: &[Scope],
6125    var_name: &str,
6126    cache: &mut ScopeLookupCache,
6127) -> Option<String> {
6128    if let Some(cached) = cache
6129        .types
6130        .get(&start_scope)
6131        .and_then(|scope_cache| scope_cache.get(var_name))
6132    {
6133        return cached.clone();
6134    }
6135    let value = lookup_type_in_scopes(start_scope, scopes, var_name);
6136    cache
6137        .types
6138        .entry(start_scope)
6139        .or_default()
6140        .insert(var_name.to_string(), value.clone());
6141    value
6142}
6143
6144fn is_builtin(name: &str, config: &ScopeResolveConfig) -> bool {
6145    // Common builtins across languages
6146    if matches!(
6147        name,
6148        "None" | "True" | "False" | "null" | "undefined" | "nil"
6149    ) {
6150        return true;
6151    }
6152    config.builtins.contains(&name)
6153}
6154
6155#[cfg(test)]
6156mod tests {
6157    use super::*;
6158
6159    #[test]
6160    fn return_type_name_lookup_uses_symbol_table_order() {
6161        let mut return_type_map = HashMap::new();
6162        return_type_map.insert(
6163            "z_backup.py::function::make_conn".to_string(),
6164            "Backup".to_string(),
6165        );
6166        return_type_map.insert(
6167            "a_primary.py::function::make_conn".to_string(),
6168            "Primary".to_string(),
6169        );
6170
6171        let mut symbol_table = HashMap::new();
6172        symbol_table.insert(
6173            "make_conn".to_string(),
6174            vec![
6175                "a_primary.py::function::make_conn".to_string(),
6176                "z_backup.py::function::make_conn".to_string(),
6177            ],
6178        );
6179
6180        let by_name = deterministic_return_types_by_name(&return_type_map, &symbol_table);
6181
6182        assert_eq!(
6183            by_name.get("make_conn").map(String::as_str),
6184            Some("Primary")
6185        );
6186    }
6187
6188    #[test]
6189    fn go_package_index_entries_are_sorted() {
6190        let first_id = "pkg/foo/a.go::function::zeta".to_string();
6191        let second_id = "pkg/foo/b.go::function::alpha".to_string();
6192
6193        let mut symbol_table = HashMap::new();
6194        symbol_table.insert("zeta".to_string(), vec![first_id.clone()]);
6195        symbol_table.insert("alpha".to_string(), vec![second_id.clone()]);
6196
6197        let mut entity_map = HashMap::new();
6198        entity_map.insert(
6199            first_id.clone(),
6200            EntityInfo {
6201                id: first_id.clone(),
6202                name: "zeta".to_string(),
6203                entity_type: "function".to_string(),
6204                file_path: "pkg/foo/a.go".to_string(),
6205                parent_id: None,
6206                start_line: 1,
6207                end_line: 3,
6208            },
6209        );
6210        entity_map.insert(
6211            second_id.clone(),
6212            EntityInfo {
6213                id: second_id.clone(),
6214                name: "alpha".to_string(),
6215                entity_type: "function".to_string(),
6216                file_path: "pkg/foo/b.go".to_string(),
6217                parent_id: None,
6218                start_line: 1,
6219                end_line: 3,
6220            },
6221        );
6222
6223        let index = build_go_pkg_index(&symbol_table, &entity_map);
6224
6225        assert_eq!(
6226            index.get("foo"),
6227            Some(&vec![
6228                ("alpha".to_string(), second_id),
6229                ("zeta".to_string(), first_id),
6230            ])
6231        );
6232    }
6233}