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