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