go_brrr/callgraph/
indexer.rs

1//! Function index building for cross-file call graph analysis.
2//!
3//! Builds indexes of all function definitions in a project for efficient
4//! lookup during call resolution. Supports multiple lookup strategies:
5//! - By simple name (returns all matching functions)
6//! - By qualified name (module.Class.method)
7//! - By file + name (for resolving local calls)
8//!
9//! # Memory Efficiency
10//!
11//! Uses arena-based storage where each function is stored exactly once in a
12//! contiguous `Vec<FunctionDef>`. All lookup indexes use `usize` indices into
13//! this arena, eliminating duplicate storage and reducing memory usage by ~3x
14//! compared to storing function references in multiple HashMaps.
15
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18
19use rayon::prelude::*;
20use tree_sitter::Parser;
21
22use crate::ast::extractor::AstExtractor;
23use crate::callgraph::types::FunctionRef;
24use crate::error::Result;
25
26/// Metadata about a function definition.
27///
28/// Extends FunctionRef with additional context needed for precise resolution.
29/// Stored once in the FunctionIndex arena; all lookups return references.
30#[derive(Debug, Clone)]
31pub struct FunctionDef {
32    /// Reference to the function (file, name, qualified_name).
33    /// Stored directly (no Arc) since arena provides single ownership.
34    pub func_ref: FunctionRef,
35    /// Whether this is a method (belongs to a class)
36    pub is_method: bool,
37    /// Parent class name if this is a method
38    pub class_name: Option<String>,
39    /// Starting line number
40    pub line_number: usize,
41    /// Source language
42    pub language: String,
43    /// Simple module name (file basename without extension).
44    ///
45    /// For "pkg/subpkg/module.py" this would be "module".
46    /// Used for Python-compatible short lookups like "module.func".
47    pub simple_module: Option<String>,
48}
49
50/// Arena-based function index that stores each function once
51/// and uses indices for multiple lookup strategies.
52///
53/// Provides O(1) lookup by name, qualified name, file+name, or class+method.
54/// Handles ambiguity when multiple functions share the same name.
55///
56/// # Memory Efficiency
57///
58/// Each `FunctionDef` is stored exactly once in the `functions` arena.
59/// All lookup maps use `usize` indices into this arena, reducing memory
60/// from ~7x duplication (with Arc clones) to 1x + indices.
61///
62/// For a project with 10,000 functions:
63/// - Old: 10,000 FunctionDefs + ~50,000 Arc<FunctionRef> clones
64/// - New: 10,000 FunctionDefs + ~50,000 usize indices (8 bytes each)
65#[derive(Debug, Default)]
66pub struct FunctionIndex {
67    /// Arena storing all function definitions (single storage).
68    /// Each function is stored exactly once; indices reference into this Vec.
69    functions: Vec<FunctionDef>,
70
71    /// Index: function name -> indices into functions arena.
72    /// Used for initial candidate lookup before disambiguation.
73    by_name: HashMap<String, Vec<usize>>,
74
75    /// Index: qualified name -> index into functions arena.
76    ///
77    /// Format varies by language:
78    /// - Python: module.Class.method or module.function
79    /// - TypeScript: file/Class.method or file/function
80    /// - Go: package.Function or package.Type.Method
81    /// - Rust: crate::module::function or crate::module::Type::method
82    by_qualified: HashMap<String, usize>,
83
84    /// Index: file path -> indices into functions arena.
85    /// Used for resolving local/relative calls.
86    by_file: HashMap<String, Vec<usize>>,
87
88    /// Index: (class_name, method_name) -> indices into functions arena.
89    ///
90    /// Enables O(1) method lookup by class and method name, avoiding
91    /// full scan of all definitions. Multiple entries possible when
92    /// same class/method name exists in different files.
93    by_class_method: HashMap<(String, String), Vec<usize>>,
94
95    /// Index: (simple_module, func_name) -> indices into functions arena.
96    ///
97    /// Simple module is the file basename without extension (e.g., "module" from
98    /// "pkg/subpkg/module.py"). This enables lookups like "module.func" to work
99    /// even when the full qualified name is "pkg.subpkg.module.func".
100    ///
101    /// This matches Python's 4-key indexing strategy:
102    /// 1. (module_name, func_name) - full module path
103    /// 2. (simple_module, func_name) - basename only (THIS FIELD)
104    /// 3. "module_name.func_name" - string key in by_qualified
105    /// 4. "simple_module.func_name" - string key in by_qualified
106    by_simple_module: HashMap<(String, String), Vec<usize>>,
107
108    /// Statistics for debugging
109    pub stats: IndexStats,
110}
111
112/// Index building statistics.
113#[derive(Debug, Default, Clone)]
114pub struct IndexStats {
115    /// Total files processed
116    pub files_processed: usize,
117    /// Files that failed to parse
118    pub parse_errors: usize,
119    /// Total functions indexed
120    pub functions_indexed: usize,
121    /// Total methods indexed
122    pub methods_indexed: usize,
123}
124
125/// Result of extracting functions from a single file.
126struct FileExtraction {
127    functions: Vec<FunctionDef>,
128}
129
130impl FunctionIndex {
131    /// Build index from a list of source files using parallel extraction.
132    ///
133    /// Uses rayon for parallel file processing, then merges results.
134    ///
135    /// # Arguments
136    /// * `files` - List of source file paths to index
137    ///
138    /// # Returns
139    /// * `Result<Self>` - Built index or error
140    pub fn build(files: &[PathBuf]) -> Result<Self> {
141        Self::build_with_root(files, None)
142    }
143
144    /// Build index with an explicit project root for relative path calculation.
145    ///
146    /// # Arguments
147    /// * `files` - List of source file paths to index
148    /// * `root` - Optional project root for computing relative paths
149    ///
150    /// # Returns
151    /// * `Result<Self>` - Built index or error
152    pub fn build_with_root(files: &[PathBuf], root: Option<&Path>) -> Result<Self> {
153        let mut index = Self::default();
154
155        // Extract functions in parallel
156        let results: Vec<FileExtraction> = files
157            .par_iter()
158            .filter_map(|path| {
159                match extract_functions_from_file(path, root) {
160                    Ok(extraction) => Some(extraction),
161                    Err(_) => {
162                        // Parse errors are expected for some files
163                        None
164                    }
165                }
166            })
167            .collect();
168
169        // Track parse errors (files that didn't produce results)
170        let successful_files = results.len();
171        index.stats.parse_errors = files.len().saturating_sub(successful_files);
172        index.stats.files_processed = successful_files;
173
174        // Merge results into index
175        for extraction in results {
176            for func_def in extraction.functions {
177                index.insert(func_def);
178            }
179        }
180
181        Ok(index)
182    }
183
184    /// Insert a function definition into the arena and all indexes.
185    ///
186    /// Creates multiple lookup indices per function:
187    /// 1. by_name: function name -> arena indices
188    /// 2. by_qualified: "module.func" -> arena index
189    /// 3. by_file: file path -> arena indices
190    /// 4. by_class_method: (class, method) -> arena indices
191    /// 5. by_simple_module: (simple_module, func) -> arena indices
192    ///
193    /// Each function is stored exactly once in the arena; indices are O(1) to store.
194    fn insert(&mut self, func_def: FunctionDef) {
195        let idx = self.functions.len();
196        let name = func_def.func_ref.name.clone();
197        let file = func_def.func_ref.file.clone();
198
199        // Update statistics
200        if func_def.is_method {
201            self.stats.methods_indexed += 1;
202        } else {
203            self.stats.functions_indexed += 1;
204        }
205
206        // Index by simple name
207        self.by_name.entry(name.clone()).or_default().push(idx);
208
209        // Index by qualified name (full module path)
210        if let Some(ref qname) = func_def.func_ref.qualified_name {
211            self.by_qualified.insert(qname.clone(), idx);
212        }
213
214        // Index by simple module (Python compatibility: "module.func" lookups)
215        if let Some(ref simple_module) = func_def.simple_module {
216            // Add to by_simple_module: (simple_module, func_name) -> Vec<usize>
217            let key = (simple_module.clone(), name.clone());
218            self.by_simple_module.entry(key).or_default().push(idx);
219
220            // Also add "simple_module.func_name" to by_qualified for string lookups
221            // This matches Python's: index[f"{simple_module}.{node.name}"] = str(rel_path)
222            //
223            // BUG FIX: Skip simple_qname for nested class methods and nested classes.
224            // For nested items, the simple_qname loses context and creates ambiguous entries.
225            // Example: nested.Outer.Middle.Inner -> simple: nested.Inner (WRONG, loses path)
226            //
227            // Nesting detection:
228            // - For methods (is_method=true): nested if class_name contains a dot (e.g., "Outer.Middle")
229            // - For classes (is_method=false): nested if class_name is Some (parent class exists)
230            let is_nested_item = if func_def.is_method {
231                func_def
232                    .class_name
233                    .as_ref()
234                    .is_some_and(|c| c.contains('.'))
235            } else {
236                func_def.class_name.is_some()
237            };
238
239            if !is_nested_item {
240                let simple_qname =
241                    build_simple_qualified_name(simple_module, &name, &func_def.language);
242                // Only insert if different from full qualified name to avoid overwrite
243                if func_def.func_ref.qualified_name.as_ref() != Some(&simple_qname) {
244                    // Don't overwrite if a more specific definition exists
245                    self.by_qualified.entry(simple_qname).or_insert(idx);
246                }
247            }
248        }
249
250        // Index by file
251        self.by_file.entry(file).or_default().push(idx);
252
253        // Index by (class_name, method_name) for O(1) method lookup
254        if func_def.is_method {
255            if let Some(ref class_name) = func_def.class_name {
256                let key = (class_name.clone(), name.clone());
257                self.by_class_method.entry(key).or_default().push(idx);
258            }
259        }
260
261        // Store function in arena (must be last, after all borrows of func_def)
262        self.functions.push(func_def);
263    }
264
265    /// Look up all functions with a given simple name.
266    ///
267    /// Returns all functions matching the name, which may be in different
268    /// files or classes. Use `lookup_qualified` or `lookup_in_file` for
269    /// more precise lookups.
270    ///
271    /// # Arguments
272    /// * `name` - Simple function name (without module/class prefix)
273    ///
274    /// # Returns
275    /// * `Vec<&FunctionRef>` - All matching function references
276    pub fn lookup(&self, name: &str) -> Vec<&FunctionRef> {
277        self.by_name
278            .get(name)
279            .map(|indices| {
280                indices
281                    .iter()
282                    .map(|&idx| &self.functions[idx].func_ref)
283                    .collect()
284            })
285            .unwrap_or_default()
286    }
287
288    /// Look up a function by its fully qualified name.
289    ///
290    /// # Arguments
291    /// * `qname` - Fully qualified name (e.g., "module.Class.method")
292    ///
293    /// # Returns
294    /// * `Option<&FunctionRef>` - Function reference if found
295    pub fn lookup_qualified(&self, qname: &str) -> Option<&FunctionRef> {
296        self.by_qualified
297            .get(qname)
298            .map(|&idx| &self.functions[idx].func_ref)
299    }
300
301    /// Look up a function in a specific file by name.
302    ///
303    /// Useful for resolving calls within the same file or to known modules.
304    ///
305    /// # Arguments
306    /// * `file` - File path (relative or absolute)
307    /// * `name` - Function name
308    ///
309    /// # Returns
310    /// * `Option<&FunctionRef>` - Function reference if found
311    #[allow(dead_code)]
312    pub fn lookup_in_file(&self, file: &str, name: &str) -> Option<&FunctionRef> {
313        self.by_file.get(file).and_then(|indices| {
314            indices
315                .iter()
316                .find(|&&idx| self.functions[idx].func_ref.name == name)
317                .map(|&idx| &self.functions[idx].func_ref)
318        })
319    }
320
321    /// Look up a method in a specific class.
322    ///
323    /// Uses the `by_class_method` secondary index for O(1) average-case lookup.
324    /// Returns all methods matching the class and method name (may exist in
325    /// multiple files with the same class name).
326    ///
327    /// # Arguments
328    /// * `class_name` - Name of the class
329    /// * `method_name` - Name of the method
330    ///
331    /// # Returns
332    /// * `Vec<&FunctionRef>` - All matching method references
333    ///
334    /// # Performance
335    /// O(1) average case via HashMap lookup, O(k) where k is the number of
336    /// methods with the same class/method name in different files.
337    #[allow(dead_code)]
338    pub fn lookup_method(&self, class_name: &str, method_name: &str) -> Vec<&FunctionRef> {
339        let key = (class_name.to_string(), method_name.to_string());
340        self.by_class_method
341            .get(&key)
342            .map(|indices| {
343                indices
344                    .iter()
345                    .map(|&idx| &self.functions[idx].func_ref)
346                    .collect()
347            })
348            .unwrap_or_default()
349    }
350
351    /// Look up functions by simple module name and function name.
352    ///
353    /// Enables lookups like "module.func" even when the full qualified name
354    /// is "pkg.subpkg.module.func". This matches Python's indexing strategy
355    /// where both full and simple module names are indexed.
356    ///
357    /// # Arguments
358    /// * `simple_module` - Module basename without extension (e.g., "utils", "helper")
359    /// * `func_name` - Function name
360    ///
361    /// # Returns
362    /// * `Vec<&FunctionRef>` - All matching function references
363    ///
364    /// # Performance
365    /// O(1) average case via HashMap lookup.
366    ///
367    /// # Example
368    /// ```ignore
369    /// // For a function defined in pkg/subpkg/utils.py:
370    /// index.lookup_simple("utils", "helper_func") // Finds it!
371    /// ```
372    #[allow(dead_code)]
373    pub fn lookup_simple(&self, simple_module: &str, func_name: &str) -> Vec<&FunctionRef> {
374        let key = (simple_module.to_string(), func_name.to_string());
375        self.by_simple_module
376            .get(&key)
377            .map(|indices| {
378                indices
379                    .iter()
380                    .map(|&idx| &self.functions[idx].func_ref)
381                    .collect()
382            })
383            .unwrap_or_default()
384    }
385
386    /// Get full metadata for a function by qualified name.
387    ///
388    /// # Arguments
389    /// * `qname` - Fully qualified name
390    ///
391    /// # Returns
392    /// * `Option<&FunctionDef>` - Full function metadata if found
393    #[allow(dead_code)]
394    pub fn get_definition(&self, qname: &str) -> Option<&FunctionDef> {
395        self.by_qualified
396            .get(qname)
397            .map(|&idx| &self.functions[idx])
398    }
399
400    /// Get all functions in the index.
401    #[allow(dead_code)]
402    pub fn all_functions(&self) -> impl Iterator<Item = &FunctionRef> {
403        self.functions.iter().map(|def| &def.func_ref)
404    }
405
406    /// Get the number of indexed functions.
407    #[allow(dead_code)]
408    pub fn len(&self) -> usize {
409        self.functions.len()
410    }
411
412    /// Check if the index is empty.
413    #[allow(dead_code)]
414    pub fn is_empty(&self) -> bool {
415        self.functions.is_empty()
416    }
417
418    /// Get all file paths in the index.
419    #[allow(dead_code)]
420    pub fn files(&self) -> impl Iterator<Item = &String> {
421        self.by_file.keys()
422    }
423
424    /// Check if a function exists by simple name.
425    #[allow(dead_code)]
426    pub fn contains(&self, name: &str) -> bool {
427        self.by_name.contains_key(name)
428    }
429
430    /// Get statistics about the index.
431    #[allow(dead_code)]
432    pub fn statistics(&self) -> &IndexStats {
433        &self.stats
434    }
435
436    /// Iterate over all function definitions.
437    ///
438    /// Provides access to full FunctionDef metadata for all indexed functions.
439    #[allow(dead_code)]
440    pub fn iter(&self) -> impl Iterator<Item = &FunctionDef> {
441        self.functions.iter()
442    }
443}
444
445/// Recursively collect (name, line_number) tuples of all nested classes.
446///
447/// This is used to identify which classes in `module_info.classes` are nested
448/// (and should be skipped in the top-level loop). The tree-sitter query returns
449/// ALL class definitions including nested ones, but we only want to process
450/// top-level classes directly - nested classes are handled via recursion.
451///
452/// Using (name, line_number) tuple instead of just name disambiguates classes
453/// with the same name in different scopes.
454fn collect_nested_class_ids<'a>(
455    classes: &'a [crate::ast::types::ClassInfo],
456) -> std::collections::HashSet<(&'a str, usize)> {
457    let mut nested_ids = std::collections::HashSet::new();
458
459    fn collect_inner<'a>(
460        class: &'a crate::ast::types::ClassInfo,
461        result: &mut std::collections::HashSet<(&'a str, usize)>,
462    ) {
463        for inner in &class.inner_classes {
464            result.insert((inner.name.as_str(), inner.line_number));
465            collect_inner(inner, result);
466        }
467    }
468
469    for class in classes {
470        collect_inner(class, &mut nested_ids);
471    }
472
473    nested_ids
474}
475
476/// Recursively collect (name, line_number) tuples of all methods in all classes,
477/// including methods in nested classes.
478///
479/// This is used to identify which functions in `module_info.functions` are actually
480/// class methods (and should be skipped when processing top-level functions).
481/// Tree-sitter queries can match methods both as class members AND as standalone
482/// functions, so we need to deduplicate.
483fn collect_all_method_identities<'a>(
484    classes: &'a [crate::ast::types::ClassInfo],
485) -> std::collections::HashSet<(&'a str, usize)> {
486    let mut method_ids = std::collections::HashSet::new();
487
488    fn collect_from_class<'a>(
489        class: &'a crate::ast::types::ClassInfo,
490        result: &mut std::collections::HashSet<(&'a str, usize)>,
491    ) {
492        // Collect methods from this class
493        for method in &class.methods {
494            result.insert((method.name.as_str(), method.line_number));
495        }
496        // Recursively collect from nested classes
497        for inner in &class.inner_classes {
498            collect_from_class(inner, result);
499        }
500    }
501
502    for class in classes {
503        collect_from_class(class, &mut method_ids);
504    }
505
506    method_ids
507}
508
509/// Extract all function definitions from a single source file.
510///
511/// Parses the file, extracts functions and class methods, and builds
512/// qualified names appropriate for the source language.
513fn extract_functions_from_file(path: &PathBuf, root: Option<&Path>) -> Result<FileExtraction> {
514    let module_info = AstExtractor::extract_file(path)?;
515
516    // Compute relative path for qualified names
517    let rel_path = if let Some(root) = root {
518        path.strip_prefix(root)
519            .map(|p| p.to_path_buf())
520            .unwrap_or_else(|_| path.clone())
521    } else {
522        path.clone()
523    };
524
525    let file_str = path.display().to_string();
526    let mut functions = Vec::new();
527
528    // Compute module name from path
529    // For Go, we need to parse the package declaration from the source file
530    // using the absolute path (not rel_path) to ensure file reading works.
531    let module_name = if module_info.language == "go" {
532        // Use absolute path to read and parse Go package declaration
533        get_go_module_name(path, None)
534    } else {
535        compute_module_name(&rel_path, &module_info.language)
536    };
537
538    // Compute simple module name (file basename without extension)
539    // For "pkg/subpkg/module.py" this would be "module"
540    let simple_module = path
541        .file_stem()
542        .and_then(|s| s.to_str())
543        .map(|s| s.to_string());
544
545    // First, collect all method identities from classes (including nested classes) to avoid duplicates.
546    // The function_query in some languages (like Python) matches methods inside
547    // classes, causing them to appear in both module_info.functions AND
548    // module_info.classes[].methods. We prioritize class methods since they
549    // have proper class context.
550    //
551    // BUG FIX: Use (name, line_number) tuple instead of just line_number for
552    // deduplication. Using only line numbers is fragile because:
553    // - Multiple lambdas or functions can start on the same line (minified code)
554    // - Two different entities on the same line would cause one to be incorrectly skipped
555    // The (name, line_number) tuple correctly identifies the same function appearing
556    // in both module_info.functions and module_info.classes[].methods.
557    //
558    // BUG FIX: Recursively collect methods from inner_classes too. Methods in nested
559    // classes were being indexed as top-level functions because they weren't in
560    // method_identities.
561    let method_identities: std::collections::HashSet<(&str, usize)> =
562        collect_all_method_identities(&module_info.classes);
563
564    // Extract top-level functions (skip those that are actually class methods)
565    for func in &module_info.functions {
566        // Skip if this function is actually a method we'll index from a class.
567        // Match by both name AND line number to avoid false positives.
568        if method_identities.contains(&(func.name.as_str(), func.line_number)) {
569            continue;
570        }
571
572        let qname = build_qualified_name(&module_name, None, &func.name, &module_info.language);
573
574        // INVARIANT: is_method == true requires class_name.is_some()
575        // Top-level functions are NEVER methods, regardless of what FunctionInfo.is_method
576        // says. Python's is_method detection based on `self` parameter can incorrectly
577        // flag standalone functions that happen to have a `self` parameter.
578        functions.push(FunctionDef {
579            func_ref: FunctionRef {
580                file: file_str.clone(),
581                name: func.name.clone(),
582                qualified_name: Some(qname),
583            },
584            is_method: false, // Top-level functions are never methods
585            class_name: None,
586            line_number: func.line_number,
587            language: module_info.language.clone(),
588            simple_module: simple_module.clone(),
589        });
590    }
591
592    // Extract class methods recursively (handles nested classes)
593    //
594    // Note: The tree-sitter query matches ALL class definitions including nested ones,
595    // so `module_info.classes` contains both top-level and nested classes. We need to
596    // identify and skip nested classes since they'll be indexed via recursion from their
597    // parent class (with proper qualified names like Outer.Middle.Inner.method).
598    //
599    // Strategy: Build a set of all nested class names (collected from inner_classes fields),
600    // then skip any class in the top-level loop that matches by (name, line_number) tuple.
601    // Using line_number disambiguates classes with the same name in different scopes.
602    let nested_class_ids: std::collections::HashSet<(&str, usize)> =
603        collect_nested_class_ids(&module_info.classes);
604
605    for class in &module_info.classes {
606        // Skip classes that appear as nested classes (identified by name + line number)
607        // These will be indexed via recursion from their parent with proper qualified names.
608        if nested_class_ids.contains(&(class.name.as_str(), class.line_number)) {
609            continue;
610        }
611
612        // Also skip classes marked with nested_in: decorator (belt and suspenders)
613        let is_nested_by_decorator = class.decorators.iter().any(|d| d.starts_with("nested_in:"));
614        if is_nested_by_decorator {
615            continue;
616        }
617
618        index_class_recursive(
619            class,
620            None, // No parent class for top-level classes
621            &module_name,
622            &file_str,
623            &module_info.language,
624            &simple_module,
625            &mut functions,
626        );
627    }
628
629    Ok(FileExtraction { functions })
630}
631
632/// Recursively index a class and all its nested classes.
633///
634/// This function handles nested class hierarchies by tracking the parent class path.
635/// For a nested class structure like:
636///
637/// ```python
638/// class Outer:
639///     class Middle:
640///         class Inner:
641///             def deep_method(self): pass
642/// ```
643///
644/// It generates qualified names like:
645/// - `module.Outer`
646/// - `module.Outer.Middle`
647/// - `module.Outer.Middle.Inner`
648/// - `module.Outer.Middle.Inner.deep_method`
649///
650/// # Arguments
651/// * `class` - The class to index
652/// * `parent_class_path` - Full path of parent classes (e.g., "Outer.Middle" for Inner)
653/// * `module_name` - Module name prefix
654/// * `file_str` - File path string
655/// * `language` - Source language
656/// * `simple_module` - Simple module name (basename without extension)
657/// * `functions` - Output vector to accumulate function definitions
658fn index_class_recursive(
659    class: &crate::ast::types::ClassInfo,
660    parent_class_path: Option<&str>,
661    module_name: &str,
662    file_str: &str,
663    language: &str,
664    simple_module: &Option<String>,
665    functions: &mut Vec<FunctionDef>,
666) {
667    // Build the full class path including parent context
668    let full_class_path = match parent_class_path {
669        Some(parent) => format!("{}.{}", parent, class.name),
670        None => class.name.clone(),
671    };
672
673    // Index all methods with the full class path
674    for method in &class.methods {
675        let qname = build_qualified_name(module_name, Some(&full_class_path), &method.name, language);
676
677        functions.push(FunctionDef {
678            func_ref: FunctionRef {
679                file: file_str.to_string(),
680                name: method.name.clone(),
681                qualified_name: Some(qname),
682            },
683            is_method: true,
684            class_name: Some(full_class_path.clone()),
685            line_number: method.line_number,
686            language: language.to_string(),
687            simple_module: simple_module.clone(),
688        });
689    }
690
691    // Index the class itself (for constructor calls)
692    let class_qname = build_qualified_name(module_name, parent_class_path, &class.name, language);
693
694    functions.push(FunctionDef {
695        func_ref: FunctionRef {
696            file: file_str.to_string(),
697            name: class.name.clone(),
698            qualified_name: Some(class_qname),
699        },
700        is_method: false,
701        class_name: parent_class_path.map(|s| s.to_string()),
702        line_number: class.line_number,
703        language: language.to_string(),
704        simple_module: simple_module.clone(),
705    });
706
707    // Recursively index nested classes
708    for inner_class in &class.inner_classes {
709        index_class_recursive(
710            inner_class,
711            Some(&full_class_path),
712            module_name,
713            file_str,
714            language,
715            simple_module,
716            functions,
717        );
718    }
719}
720
721/// Extract Go package name from source code using tree-sitter.
722///
723/// Go package names come from the `package` declaration in the source file,
724/// NOT from the directory name. This is critical for correct module naming.
725///
726/// # Examples
727/// - `cmd/myapp/main.go` with `package main` returns Some("main")
728/// - `internal/utils/helper.go` with `package utils` returns Some("utils")
729///
730/// # Arguments
731/// * `source` - Source code bytes
732///
733/// # Returns
734/// * `Option<String>` - Package name if found, None if parsing fails
735fn extract_go_package_name(source: &[u8]) -> Option<String> {
736    let mut parser = Parser::new();
737    parser
738        .set_language(&tree_sitter_go::LANGUAGE.into())
739        .ok()?;
740
741    let tree = parser.parse(source, None)?;
742    let root = tree.root_node();
743
744    // Go AST structure: source_file -> package_clause -> package_identifier
745    // The package_clause is typically the first child of source_file
746    let mut cursor = root.walk();
747    for child in root.children(&mut cursor) {
748        if child.kind() == "package_clause" {
749            // Find the package_identifier within the package_clause
750            let mut inner_cursor = child.walk();
751            for inner_child in child.children(&mut inner_cursor) {
752                if inner_child.kind() == "package_identifier" {
753                    let name = inner_child.utf8_text(source).ok()?.to_string();
754                    return Some(name);
755                }
756            }
757        }
758    }
759
760    None
761}
762
763/// Extract Go package name with fallback to directory name.
764///
765/// Attempts to parse the package declaration from the source file.
766/// Falls back to the parent directory name if parsing fails.
767///
768/// # Arguments
769/// * `path` - Path to the Go source file
770/// * `source` - Source code bytes (optional, read from file if None)
771///
772/// # Returns
773/// * `String` - Package name (from source or fallback)
774fn get_go_module_name(path: &Path, source: Option<&[u8]>) -> String {
775    // Try to extract from source
776    let package_name = match source {
777        Some(src) => extract_go_package_name(src),
778        None => {
779            // Read file if source not provided
780            std::fs::read(path)
781                .ok()
782                .and_then(|bytes| extract_go_package_name(&bytes))
783        }
784    };
785
786    if let Some(name) = package_name {
787        return name;
788    }
789
790    // Fallback: use directory name (original behavior)
791    path.parent()
792        .and_then(|p| p.file_name())
793        .and_then(|n| n.to_str())
794        .unwrap_or("main")
795        .to_string()
796}
797
798/// Compute module name from file path based on language conventions.
799///
800/// - Python: path/to/module.py -> path.to.module
801/// - TypeScript: path/to/module.ts -> path/to/module
802/// - Go: package name from `package` declaration (NOT directory name)
803/// - Rust: path::to::module
804///
805/// Note: For Go, use `compute_module_name_go` which accepts source code
806/// for accurate package name extraction.
807fn compute_module_name(path: &Path, language: &str) -> String {
808    let stem = path
809        .file_stem()
810        .and_then(|s| s.to_str())
811        .unwrap_or("unknown");
812
813    let parent_parts: Vec<&str> = path
814        .parent()
815        .map(|p| p.iter().filter_map(|c| c.to_str()).collect())
816        .unwrap_or_default();
817
818    match language {
819        "python" => {
820            if parent_parts.is_empty() {
821                stem.to_string()
822            } else {
823                format!("{}.{}", parent_parts.join("."), stem)
824            }
825        }
826        "typescript" | "javascript" => {
827            if parent_parts.is_empty() {
828                stem.to_string()
829            } else {
830                format!("{}/{}", parent_parts.join("/"), stem)
831            }
832        }
833        "go" => {
834            // Go package name comes from the `package` declaration in source.
835            // Use get_go_module_name which reads the file and parses the package.
836            // This is called from compute_module_name which doesn't have source,
837            // so we pass None to trigger file reading.
838            get_go_module_name(path, None)
839        }
840        "rust" => {
841            if parent_parts.is_empty() {
842                stem.to_string()
843            } else {
844                format!("{}::{}", parent_parts.join("::"), stem)
845            }
846        }
847        "java" => {
848            // Java uses package.ClassName
849            if parent_parts.is_empty() {
850                stem.to_string()
851            } else {
852                format!("{}.{}", parent_parts.join("."), stem)
853            }
854        }
855        "c" => {
856            // C doesn't have namespaces, use file stem
857            stem.to_string()
858        }
859        _ => stem.to_string(),
860    }
861}
862
863/// Build a fully qualified name for a function or method.
864///
865/// Format varies by language:
866/// - Python: module.Class.method or module.function
867/// - TypeScript: module/Class.method or module/function (module separator is /, class-method is .)
868/// - Go: package.Type.Method or package.Function
869/// - Rust: module::Type::method or module::function
870///
871/// # Performance
872/// Uses pre-allocated String with exact capacity to avoid reallocation.
873/// Called for every function indexed, so allocation efficiency matters.
874#[inline]
875fn build_qualified_name(module: &str, class: Option<&str>, name: &str, language: &str) -> String {
876    // Determine separators based on language
877    // TypeScript/JavaScript: module separator is /, but class.method uses .
878    // Rust/C: use :: for everything
879    // Others (Python, Java, Go): use . for everything
880    let (module_sep, class_sep) = match language {
881        "typescript" | "javascript" => ("/", "."),
882        "rust" | "c" => ("::", "::"),
883        _ => (".", "."), // Python, Java, Go, and default
884    };
885
886    // Calculate exact capacity needed to avoid reallocation
887    let capacity = module.len()
888        + module_sep.len()
889        + class.map(|c| c.len() + class_sep.len()).unwrap_or(0)
890        + name.len();
891
892    let mut result = String::with_capacity(capacity);
893
894    result.push_str(module);
895
896    if let Some(c) = class {
897        result.push_str(module_sep);
898        result.push_str(c);
899        result.push_str(class_sep);
900        result.push_str(name);
901    } else {
902        result.push_str(module_sep);
903        result.push_str(name);
904    }
905
906    result
907}
908
909/// Build a simple qualified name using just the module basename and function name.
910///
911/// This is used for Python-compatible short lookups like "module.func" where
912/// "module" is just the filename without path (e.g., "utils" from "pkg/utils.py").
913///
914/// Format varies by language separator:
915/// - Python/Java/Go: module.func
916/// - TypeScript/JavaScript: module/func
917/// - Rust/C: module::func
918///
919/// # Performance
920/// Uses pre-allocated String with exact capacity to avoid reallocation.
921/// Called for every function indexed, so allocation efficiency matters.
922#[inline]
923fn build_simple_qualified_name(simple_module: &str, name: &str, language: &str) -> String {
924    let sep = match language {
925        "rust" | "c" => "::",
926        "typescript" | "javascript" => "/",
927        _ => ".",
928    };
929
930    // Calculate exact capacity: module + separator + name
931    let capacity = simple_module.len() + sep.len() + name.len();
932    let mut result = String::with_capacity(capacity);
933
934    result.push_str(simple_module);
935    result.push_str(sep);
936    result.push_str(name);
937
938    result
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944    use std::io::Write;
945    use tempfile::TempDir;
946
947    fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
948        let path = dir.path().join(name);
949        if let Some(parent) = path.parent() {
950            std::fs::create_dir_all(parent).unwrap();
951        }
952        let mut file = std::fs::File::create(&path).unwrap();
953        file.write_all(content.as_bytes()).unwrap();
954        path
955    }
956
957    #[test]
958    fn test_build_index_python() {
959        let dir = TempDir::new().unwrap();
960
961        let content = r#"
962def standalone():
963    pass
964
965class MyClass:
966    def method(self):
967        pass
968
969async def async_func():
970    pass
971"#;
972        let file = create_temp_file(&dir, "module.py", content);
973
974        let index = FunctionIndex::build(&[file]).unwrap();
975
976        // Check statistics
977        assert!(index.stats.files_processed >= 1);
978        assert!(index.len() >= 3); // standalone, method, async_func, MyClass
979
980        // Lookup by simple name
981        let funcs = index.lookup("standalone");
982        assert!(!funcs.is_empty());
983
984        let methods = index.lookup("method");
985        assert!(!methods.is_empty());
986    }
987
988    #[test]
989    fn test_build_index_typescript() {
990        let dir = TempDir::new().unwrap();
991
992        let content = r#"
993function greet(name: string): void {
994    console.log(name);
995}
996
997class Service {
998    handle(): void {}
999}
1000
1001const arrow = () => {};
1002"#;
1003        let file = create_temp_file(&dir, "api.ts", content);
1004
1005        let index = FunctionIndex::build(&[file]).unwrap();
1006
1007        // Check we found functions
1008        assert!(!index.is_empty());
1009
1010        let greet = index.lookup("greet");
1011        assert!(!greet.is_empty());
1012    }
1013
1014    #[test]
1015    fn test_lookup_qualified() {
1016        let dir = TempDir::new().unwrap();
1017
1018        let content = r#"
1019class Controller:
1020    def handle(self):
1021        pass
1022"#;
1023        let file = create_temp_file(&dir, "web.py", content);
1024
1025        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1026
1027        // Should be able to look up with qualified name
1028        let result = index.lookup_qualified("web.Controller.handle");
1029        assert!(result.is_some());
1030    }
1031
1032    #[test]
1033    fn test_lookup_in_file() {
1034        let dir = TempDir::new().unwrap();
1035
1036        let content = r#"
1037def helper():
1038    pass
1039
1040def main():
1041    helper()
1042"#;
1043        let file = create_temp_file(&dir, "script.py", content);
1044        let file_str = file.display().to_string();
1045
1046        let index = FunctionIndex::build(&[file]).unwrap();
1047
1048        let result = index.lookup_in_file(&file_str, "helper");
1049        assert!(result.is_some());
1050        assert_eq!(result.unwrap().name, "helper");
1051    }
1052
1053    #[test]
1054    fn test_lookup_method() {
1055        let dir = TempDir::new().unwrap();
1056
1057        let content = r#"
1058class Service:
1059    def process(self):
1060        pass
1061
1062class Handler:
1063    def process(self):
1064        pass
1065"#;
1066        let file = create_temp_file(&dir, "handlers.py", content);
1067
1068        let index = FunctionIndex::build(&[file]).unwrap();
1069
1070        // Both classes have 'process', lookup should find both
1071        let all_process = index.lookup("process");
1072        assert_eq!(all_process.len(), 2);
1073
1074        // Lookup specific class method
1075        let service_process = index.lookup_method("Service", "process");
1076        assert_eq!(service_process.len(), 1);
1077
1078        let handler_process = index.lookup_method("Handler", "process");
1079        assert_eq!(handler_process.len(), 1);
1080    }
1081
1082    #[test]
1083    fn test_multiple_files_same_function_name() {
1084        let dir = TempDir::new().unwrap();
1085
1086        let content1 = "def helper(): pass";
1087        let content2 = "def helper(): pass";
1088
1089        let file1 = create_temp_file(&dir, "module1.py", content1);
1090        let file2 = create_temp_file(&dir, "module2.py", content2);
1091
1092        let index = FunctionIndex::build(&[file1, file2]).unwrap();
1093
1094        // Should find both functions with same name
1095        let helpers = index.lookup("helper");
1096        assert_eq!(helpers.len(), 2);
1097    }
1098
1099    #[test]
1100    fn test_compute_module_name_python() {
1101        let path = Path::new("pkg/subpkg/module.py");
1102        let module = compute_module_name(path, "python");
1103        assert_eq!(module, "pkg.subpkg.module");
1104    }
1105
1106    #[test]
1107    fn test_compute_module_name_typescript() {
1108        let path = Path::new("src/utils/helpers.ts");
1109        let module = compute_module_name(path, "typescript");
1110        assert_eq!(module, "src/utils/helpers");
1111    }
1112
1113    #[test]
1114    fn test_compute_module_name_rust() {
1115        let path = Path::new("src/lib/parser.rs");
1116        let module = compute_module_name(path, "rust");
1117        assert_eq!(module, "src::lib::parser");
1118    }
1119
1120    #[test]
1121    fn test_build_qualified_name_python() {
1122        let qname = build_qualified_name("module", Some("Class"), "method", "python");
1123        assert_eq!(qname, "module.Class.method");
1124
1125        let qname = build_qualified_name("module", None, "func", "python");
1126        assert_eq!(qname, "module.func");
1127    }
1128
1129    #[test]
1130    fn test_build_qualified_name_typescript() {
1131        let qname = build_qualified_name("utils", Some("Helper"), "run", "typescript");
1132        assert_eq!(qname, "utils/Helper.run");
1133
1134        let qname = build_qualified_name("utils", None, "parse", "typescript");
1135        assert_eq!(qname, "utils/parse");
1136    }
1137
1138    #[test]
1139    fn test_build_qualified_name_rust() {
1140        let qname = build_qualified_name("parser", Some("Lexer"), "tokenize", "rust");
1141        assert_eq!(qname, "parser::Lexer::tokenize");
1142
1143        let qname = build_qualified_name("utils", None, "helper", "rust");
1144        assert_eq!(qname, "utils::helper");
1145    }
1146
1147    #[test]
1148    fn test_empty_index() {
1149        let index = FunctionIndex::default();
1150
1151        assert!(index.is_empty());
1152        assert_eq!(index.len(), 0);
1153        assert!(index.lookup("anything").is_empty());
1154        assert!(index.lookup_qualified("any.thing").is_none());
1155    }
1156
1157    #[test]
1158    fn test_class_indexed_for_constructors() {
1159        let dir = TempDir::new().unwrap();
1160
1161        let content = r#"
1162class MyService:
1163    def __init__(self):
1164        pass
1165"#;
1166        let file = create_temp_file(&dir, "service.py", content);
1167
1168        let index = FunctionIndex::build(&[file]).unwrap();
1169
1170        // The class itself should be indexed (for constructor calls like MyService())
1171        let classes = index.lookup("MyService");
1172        assert!(!classes.is_empty());
1173    }
1174
1175    #[test]
1176    fn test_deduplication_uses_name_and_line_not_just_line() {
1177        // BUG FIX TEST: Verify that deduplication uses (name, line_number) tuple
1178        // instead of just line_number. This prevents incorrectly skipping
1179        // different functions that happen to start on the same line
1180        // (e.g., multiple lambdas in minified code).
1181        let dir = TempDir::new().unwrap();
1182
1183        // Python code with a method and a standalone function
1184        // The method should be indexed from the class, and the standalone
1185        // function should be indexed separately even if they share a name
1186        // with different semantics (method vs function).
1187        let content = r#"
1188def helper():
1189    """Standalone function"""
1190    pass
1191
1192class MyClass:
1193    def process(self):
1194        """Method with unique name"""
1195        pass
1196
1197def process_data():
1198    """Another standalone function"""
1199    pass
1200"#;
1201        let file = create_temp_file(&dir, "module.py", content);
1202        let index = FunctionIndex::build(&[file]).unwrap();
1203
1204        // All three should be indexed:
1205        // 1. helper (standalone function)
1206        // 2. process (method of MyClass)
1207        // 3. process_data (standalone function)
1208        let helpers = index.lookup("helper");
1209        assert_eq!(helpers.len(), 1, "helper function should be indexed");
1210
1211        let processes = index.lookup("process");
1212        assert_eq!(processes.len(), 1, "process method should be indexed");
1213
1214        let process_datas = index.lookup("process_data");
1215        assert_eq!(
1216            process_datas.len(),
1217            1,
1218            "process_data function should be indexed"
1219        );
1220
1221        // Also verify the method lookup works
1222        let method = index.lookup_method("MyClass", "process");
1223        assert_eq!(
1224            method.len(),
1225            1,
1226            "MyClass.process method should be found via method lookup"
1227        );
1228    }
1229
1230    #[test]
1231    fn test_deduplication_skips_method_appearing_in_functions_list() {
1232        // Verify that methods appearing in both module_info.functions and
1233        // module_info.classes[].methods are correctly deduplicated (indexed
1234        // only once, from the class context which has proper parent info).
1235        let dir = TempDir::new().unwrap();
1236
1237        let content = r#"
1238class Controller:
1239    def handle(self):
1240        pass
1241
1242    def process(self):
1243        pass
1244"#;
1245        let file = create_temp_file(&dir, "api.py", content);
1246        let index = FunctionIndex::build(&[file]).unwrap();
1247
1248        // Each method should appear exactly once
1249        let handles = index.lookup("handle");
1250        assert_eq!(
1251            handles.len(),
1252            1,
1253            "handle should appear exactly once (not duplicated)"
1254        );
1255
1256        let processes = index.lookup("process");
1257        assert_eq!(
1258            processes.len(),
1259            1,
1260            "process should appear exactly once (not duplicated)"
1261        );
1262
1263        // Both should be found as methods of Controller
1264        assert_eq!(index.lookup_method("Controller", "handle").len(), 1);
1265        assert_eq!(index.lookup_method("Controller", "process").len(), 1);
1266    }
1267
1268    #[test]
1269    fn test_simple_module_lookup() {
1270        // Test Python-compatible simple module lookups (4-key indexing strategy)
1271        let dir = TempDir::new().unwrap();
1272
1273        let content = r#"
1274def helper():
1275    pass
1276
1277class Service:
1278    def process(self):
1279        pass
1280"#;
1281        // Create file in nested directory: pkg/subpkg/utils.py
1282        let file = create_temp_file(&dir, "pkg/subpkg/utils.py", content);
1283
1284        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1285
1286        // 1. Full qualified name should work
1287        let result = index.lookup_qualified("pkg.subpkg.utils.helper");
1288        assert!(result.is_some(), "full qualified lookup should work");
1289
1290        // 2. Simple module lookup should work (utils.helper)
1291        let result = index.lookup_simple("utils", "helper");
1292        assert!(
1293            !result.is_empty(),
1294            "simple module lookup (utils, helper) should work"
1295        );
1296
1297        // 3. Method with simple module should also work
1298        let result = index.lookup_simple("utils", "process");
1299        assert!(
1300            !result.is_empty(),
1301            "simple module lookup for method should work"
1302        );
1303    }
1304
1305    #[test]
1306    fn test_simple_module_typescript() {
1307        // Test simple module lookup for TypeScript (uses / separator)
1308        let dir = TempDir::new().unwrap();
1309
1310        let content = r#"
1311function greet(name: string): void {
1312    console.log(name);
1313}
1314"#;
1315        let file = create_temp_file(&dir, "src/utils/helpers.ts", content);
1316
1317        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1318
1319        // Simple module lookup should work
1320        let result = index.lookup_simple("helpers", "greet");
1321        assert!(
1322            !result.is_empty(),
1323            "TypeScript simple module lookup should work"
1324        );
1325    }
1326
1327    #[test]
1328    fn test_build_simple_qualified_name_helper() {
1329        // Test the build_simple_qualified_name helper
1330        assert_eq!(
1331            build_simple_qualified_name("module", "func", "python"),
1332            "module.func"
1333        );
1334        assert_eq!(
1335            build_simple_qualified_name("helpers", "greet", "typescript"),
1336            "helpers/greet"
1337        );
1338        assert_eq!(
1339            build_simple_qualified_name("parser", "tokenize", "rust"),
1340            "parser::tokenize"
1341        );
1342    }
1343
1344    // =========================================================================
1345    // Go package name extraction tests (BUG FIX)
1346    // =========================================================================
1347
1348    #[test]
1349    fn test_extract_go_package_name_main() {
1350        // Test: cmd/myapp/main.go with `package main` should return "main"
1351        let source = br#"
1352package main
1353
1354import "fmt"
1355
1356func main() {
1357    fmt.Println("Hello")
1358}
1359"#;
1360        let result = extract_go_package_name(source);
1361        assert_eq!(result, Some("main".to_string()));
1362    }
1363
1364    #[test]
1365    fn test_extract_go_package_name_utils() {
1366        // Test: internal/utils/helper.go with `package utils` should return "utils"
1367        let source = br#"
1368package utils
1369
1370func Helper() string {
1371    return "helper"
1372}
1373"#;
1374        let result = extract_go_package_name(source);
1375        assert_eq!(result, Some("utils".to_string()));
1376    }
1377
1378    #[test]
1379    fn test_extract_go_package_name_with_comment() {
1380        // Test: package declaration with preceding doc comment
1381        let source = br#"
1382// Package myserver implements a web server.
1383package myserver
1384
1385import "net/http"
1386
1387func Serve() {
1388}
1389"#;
1390        let result = extract_go_package_name(source);
1391        assert_eq!(result, Some("myserver".to_string()));
1392    }
1393
1394    #[test]
1395    fn test_extract_go_package_name_invalid_source() {
1396        // Test: invalid Go source should return None
1397        let source = b"this is not valid go code";
1398        let result = extract_go_package_name(source);
1399        // tree-sitter is lenient, but without package clause, should return None
1400        assert_eq!(result, None);
1401    }
1402
1403    #[test]
1404    fn test_go_module_name_uses_package_not_directory() {
1405        // Integration test: verify Go indexing uses package name, not directory name
1406        let dir = TempDir::new().unwrap();
1407
1408        // Create file in cmd/myapp/ but with package main
1409        let content = r#"
1410package main
1411
1412func Run() string {
1413    return "running"
1414}
1415"#;
1416        let file = create_temp_file(&dir, "cmd/myapp/main.go", content);
1417        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1418
1419        // The function should be qualified with "main" (package name),
1420        // NOT "myapp" (directory name)
1421        let result = index.lookup_qualified("main.Run");
1422        assert!(
1423            result.is_some(),
1424            "Function should be qualified as main.Run (from package declaration)"
1425        );
1426
1427        // Verify it's NOT qualified as myapp.Run (directory name - wrong behavior)
1428        let wrong_result = index.lookup_qualified("myapp.Run");
1429        assert!(
1430            wrong_result.is_none(),
1431            "Function should NOT be qualified as myapp.Run (directory name)"
1432        );
1433    }
1434
1435    #[test]
1436    fn test_go_package_utils_not_internal_utils() {
1437        // Integration test: internal/utils/helper.go with `package utils`
1438        // should be qualified as utils.Helper, not internal/utils.Helper
1439        let dir = TempDir::new().unwrap();
1440
1441        let content = r#"
1442package utils
1443
1444func Helper() string {
1445    return "helper"
1446}
1447"#;
1448        let file = create_temp_file(&dir, "internal/utils/helper.go", content);
1449        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1450
1451        // Should be qualified with "utils" (package name)
1452        let result = index.lookup_qualified("utils.Helper");
1453        assert!(
1454            result.is_some(),
1455            "Function should be qualified as utils.Helper (from package declaration)"
1456        );
1457    }
1458
1459    #[test]
1460    fn test_is_method_class_name_invariant() {
1461        // BUG FIX TEST: Verify that is_method and class_name are always consistent.
1462        // INVARIANT: is_method == true => class_name.is_some()
1463        // This prevents the bug where top-level functions with `self` parameter
1464        // were incorrectly marked as methods but had class_name: None.
1465        let dir = TempDir::new().unwrap();
1466
1467        // Python code with a standalone function that has `self` parameter
1468        // (unusual but valid - e.g., decorator implementations, factory functions)
1469        let content = r#"
1470def standalone_with_self(self, data):
1471    """A standalone function that happens to have a 'self' parameter.
1472    This should NOT be marked as a method because it's not inside a class."""
1473    return data
1474
1475def normal_function():
1476    """A normal function without self parameter."""
1477    pass
1478
1479class MyClass:
1480    def actual_method(self):
1481        """This IS a method inside a class."""
1482        pass
1483"#;
1484        let file = create_temp_file(&dir, "module.py", content);
1485        let index = FunctionIndex::build(&[file]).unwrap();
1486
1487        // Verify the invariant: is_method == true => class_name.is_some()
1488        for def in index.iter() {
1489            if def.is_method {
1490                assert!(
1491                    def.class_name.is_some(),
1492                    "INVARIANT VIOLATION: {} has is_method=true but class_name=None",
1493                    def.func_ref.qualified_name.as_deref().unwrap_or(&def.func_ref.name)
1494                );
1495            }
1496        }
1497
1498        // Specifically verify standalone_with_self is NOT marked as method
1499        let standalone = index.lookup("standalone_with_self");
1500        assert_eq!(standalone.len(), 1, "Should find standalone_with_self");
1501
1502        // Get the full definition to check is_method
1503        let qname = standalone[0].qualified_name.as_ref().unwrap();
1504        let def = index.get_definition(qname).unwrap();
1505        assert!(
1506            !def.is_method,
1507            "standalone_with_self should NOT be marked as a method even though it has 'self' param"
1508        );
1509        assert!(
1510            def.class_name.is_none(),
1511            "standalone_with_self should not have a class_name"
1512        );
1513
1514        // Verify actual_method IS correctly marked as a method
1515        let actual_method = index.lookup_method("MyClass", "actual_method");
1516        assert_eq!(actual_method.len(), 1, "Should find MyClass.actual_method");
1517
1518        let method_qname = actual_method[0].qualified_name.as_ref().unwrap();
1519        let method_def = index.get_definition(method_qname).unwrap();
1520        assert!(
1521            method_def.is_method,
1522            "actual_method should be marked as a method"
1523        );
1524        assert_eq!(
1525            method_def.class_name,
1526            Some("MyClass".to_string()),
1527            "actual_method should have class_name = MyClass"
1528        );
1529    }
1530
1531    #[test]
1532    fn test_go_package_with_method() {
1533        // Test Go method indexing with correct package name
1534        let dir = TempDir::new().unwrap();
1535
1536        let content = r#"
1537package myservice
1538
1539type Service struct {}
1540
1541func (s *Service) Run() string {
1542    return "running"
1543}
1544
1545func NewService() *Service {
1546    return &Service{}
1547}
1548"#;
1549        let file = create_temp_file(&dir, "pkg/server/service.go", content);
1550        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1551
1552        // Function should be qualified as myservice.NewService
1553        let result = index.lookup_qualified("myservice.NewService");
1554        assert!(
1555            result.is_some(),
1556            "Function should be qualified as myservice.NewService"
1557        );
1558
1559        // Go receiver methods are qualified as package.MethodName
1560        // The receiver type is stored in decorators but NOT used as class_name
1561        // because Go doesn't have classes in the traditional OOP sense.
1562        let method_result = index.lookup_qualified("myservice.Run");
1563        assert!(
1564            method_result.is_some(),
1565            "Receiver method should be qualified as myservice.Run"
1566        );
1567
1568        // Receiver method should be findable by simple name
1569        let methods = index.lookup("Run");
1570        assert!(
1571            !methods.is_empty(),
1572            "Receiver method Run should be found by simple name"
1573        );
1574
1575        // INVARIANT: is_method == true => class_name.is_some()
1576        // Go receiver methods are indexed as top-level functions because Go doesn't
1577        // have classes. The receiver type (e.g., *Service) is stored in decorators,
1578        // not as class_name. Therefore, is_method should be false to maintain the
1579        // invariant. The receiver information is preserved in FunctionInfo.decorators.
1580        let run_def = index.get_definition("myservice.Run");
1581        assert!(run_def.is_some(), "Should have definition for myservice.Run");
1582        let def = run_def.unwrap();
1583
1584        // Go receiver methods are NOT marked as is_method because they don't have
1585        // class_name set (Go uses receivers, not classes)
1586        assert!(
1587            !def.is_method || def.class_name.is_some(),
1588            "INVARIANT: is_method=true requires class_name to be set"
1589        );
1590
1591        // Since class_name is None for Go receivers, is_method should be false
1592        assert!(
1593            def.class_name.is_none(),
1594            "Go receiver methods don't have class_name (receiver is in decorators)"
1595        );
1596        assert!(
1597            !def.is_method,
1598            "Go receiver methods indexed without class_name should have is_method=false"
1599        );
1600    }
1601
1602    #[test]
1603    fn test_nested_class_qualified_names() {
1604        // BUG FIX TEST: Verify nested classes produce correct qualified names.
1605        // For nested classes like:
1606        //   class Outer:
1607        //       class Middle:
1608        //           class Inner:
1609        //               def deep_method(self): pass
1610        //
1611        // Expected qualified names:
1612        //   - module.Outer
1613        //   - module.Outer.Middle
1614        //   - module.Outer.Middle.Inner
1615        //   - module.Outer.Middle.Inner.deep_method
1616        //
1617        // Previously buggy output: module.Inner.deep_method (missing parent class path)
1618        let dir = TempDir::new().unwrap();
1619
1620        let content = r#"
1621class Outer:
1622    def outer_method(self):
1623        pass
1624
1625    class Middle:
1626        def middle_method(self):
1627            pass
1628
1629        class Inner:
1630            def deep_method(self):
1631                pass
1632"#;
1633        let file = create_temp_file(&dir, "nested.py", content);
1634        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1635
1636        // Verify top-level class
1637        let outer = index.lookup_qualified("nested.Outer");
1638        assert!(outer.is_some(), "Should find Outer class as nested.Outer");
1639
1640        // Verify first-level nested class with FULL path
1641        let middle = index.lookup_qualified("nested.Outer.Middle");
1642        assert!(
1643            middle.is_some(),
1644            "Should find Middle class as nested.Outer.Middle (not just nested.Middle)"
1645        );
1646
1647        // Verify deeply nested class with FULL path
1648        let inner = index.lookup_qualified("nested.Outer.Middle.Inner");
1649        assert!(
1650            inner.is_some(),
1651            "Should find Inner class as nested.Outer.Middle.Inner (not just nested.Inner)"
1652        );
1653
1654        // Verify method in deeply nested class has FULL qualified name
1655        let deep_method = index.lookup_qualified("nested.Outer.Middle.Inner.deep_method");
1656        assert!(
1657            deep_method.is_some(),
1658            "Should find deep_method as nested.Outer.Middle.Inner.deep_method"
1659        );
1660
1661        // Verify the method's class_name is the full path
1662        let method_def = index.get_definition("nested.Outer.Middle.Inner.deep_method");
1663        assert!(
1664            method_def.is_some(),
1665            "Should have definition for deep_method"
1666        );
1667        let def = method_def.unwrap();
1668        assert!(def.is_method, "deep_method should be marked as a method");
1669        assert_eq!(
1670            def.class_name,
1671            Some("Outer.Middle.Inner".to_string()),
1672            "deep_method's class_name should be the full nested path"
1673        );
1674
1675        // Verify outer_method is correctly qualified
1676        let outer_method = index.lookup_qualified("nested.Outer.outer_method");
1677        assert!(
1678            outer_method.is_some(),
1679            "Should find outer_method as nested.Outer.outer_method"
1680        );
1681
1682        // Verify middle_method is correctly qualified
1683        let middle_method = index.lookup_qualified("nested.Outer.Middle.middle_method");
1684        assert!(
1685            middle_method.is_some(),
1686            "Should find middle_method as nested.Outer.Middle.middle_method"
1687        );
1688
1689        // Verify lookups don't find incorrectly qualified names
1690        let wrong_inner = index.lookup_qualified("nested.Inner");
1691        assert!(
1692            wrong_inner.is_none(),
1693            "Should NOT find Inner as nested.Inner (missing parent class path)"
1694        );
1695
1696        let wrong_method = index.lookup_qualified("nested.Inner.deep_method");
1697        assert!(
1698            wrong_method.is_none(),
1699            "Should NOT find deep_method as nested.Inner.deep_method"
1700        );
1701    }
1702
1703    #[test]
1704    fn test_nested_class_method_lookup() {
1705        // Verify method lookup by (class_name, method_name) works with nested classes.
1706        // The class_name should be the full nested path.
1707        let dir = TempDir::new().unwrap();
1708
1709        let content = r#"
1710class Parent:
1711    class Child:
1712        def child_method(self):
1713            pass
1714"#;
1715        let file = create_temp_file(&dir, "hierarchy.py", content);
1716        let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
1717
1718        // lookup_method should find with full class path
1719        let found = index.lookup_method("Parent.Child", "child_method");
1720        assert!(
1721            !found.is_empty(),
1722            "Should find child_method with class_name='Parent.Child'"
1723        );
1724
1725        // lookup_method should NOT find with incomplete class path
1726        let not_found = index.lookup_method("Child", "child_method");
1727        assert!(
1728            not_found.is_empty(),
1729            "Should NOT find child_method with class_name='Child' (needs full path)"
1730        );
1731    }
1732
1733}