use std::collections::HashMap;
use std::path::{Path, PathBuf};
use rayon::prelude::*;
use tree_sitter::Parser;
use crate::ast::extractor::AstExtractor;
use crate::callgraph::types::FunctionRef;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct FunctionDef {
pub func_ref: FunctionRef,
pub is_method: bool,
pub class_name: Option<String>,
pub line_number: usize,
pub language: String,
pub simple_module: Option<String>,
}
#[derive(Debug, Default)]
pub struct FunctionIndex {
functions: Vec<FunctionDef>,
by_name: HashMap<String, Vec<usize>>,
by_qualified: HashMap<String, usize>,
by_file: HashMap<String, Vec<usize>>,
by_class_method: HashMap<(String, String), Vec<usize>>,
by_simple_module: HashMap<(String, String), Vec<usize>>,
pub stats: IndexStats,
}
#[derive(Debug, Default, Clone)]
pub struct IndexStats {
pub files_processed: usize,
pub parse_errors: usize,
pub functions_indexed: usize,
pub methods_indexed: usize,
}
struct FileExtraction {
functions: Vec<FunctionDef>,
}
impl FunctionIndex {
pub fn build(files: &[PathBuf]) -> Result<Self> {
Self::build_with_root(files, None)
}
pub fn build_with_root(files: &[PathBuf], root: Option<&Path>) -> Result<Self> {
let mut index = Self::default();
let results: Vec<FileExtraction> = files
.par_iter()
.filter_map(|path| {
match extract_functions_from_file(path, root) {
Ok(extraction) => Some(extraction),
Err(_) => {
None
}
}
})
.collect();
let successful_files = results.len();
index.stats.parse_errors = files.len().saturating_sub(successful_files);
index.stats.files_processed = successful_files;
for extraction in results {
for func_def in extraction.functions {
index.insert(func_def);
}
}
Ok(index)
}
fn insert(&mut self, func_def: FunctionDef) {
let idx = self.functions.len();
let name = func_def.func_ref.name.clone();
let file = func_def.func_ref.file.clone();
if func_def.is_method {
self.stats.methods_indexed += 1;
} else {
self.stats.functions_indexed += 1;
}
self.by_name.entry(name.clone()).or_default().push(idx);
if let Some(ref qname) = func_def.func_ref.qualified_name {
self.by_qualified.insert(qname.clone(), idx);
}
if let Some(ref simple_module) = func_def.simple_module {
let key = (simple_module.clone(), name.clone());
self.by_simple_module.entry(key).or_default().push(idx);
let is_nested_item = if func_def.is_method {
func_def
.class_name
.as_ref()
.is_some_and(|c| c.contains('.'))
} else {
func_def.class_name.is_some()
};
if !is_nested_item {
let simple_qname =
build_simple_qualified_name(simple_module, &name, &func_def.language);
if func_def.func_ref.qualified_name.as_ref() != Some(&simple_qname) {
self.by_qualified.entry(simple_qname).or_insert(idx);
}
}
}
self.by_file.entry(file).or_default().push(idx);
if func_def.is_method {
if let Some(ref class_name) = func_def.class_name {
let key = (class_name.clone(), name.clone());
self.by_class_method.entry(key).or_default().push(idx);
}
}
self.functions.push(func_def);
}
pub fn lookup(&self, name: &str) -> Vec<&FunctionRef> {
self.by_name
.get(name)
.map(|indices| {
indices
.iter()
.map(|&idx| &self.functions[idx].func_ref)
.collect()
})
.unwrap_or_default()
}
pub fn lookup_qualified(&self, qname: &str) -> Option<&FunctionRef> {
self.by_qualified
.get(qname)
.map(|&idx| &self.functions[idx].func_ref)
}
#[allow(dead_code)]
pub fn lookup_in_file(&self, file: &str, name: &str) -> Option<&FunctionRef> {
self.by_file.get(file).and_then(|indices| {
indices
.iter()
.find(|&&idx| self.functions[idx].func_ref.name == name)
.map(|&idx| &self.functions[idx].func_ref)
})
}
#[allow(dead_code)]
pub fn lookup_method(&self, class_name: &str, method_name: &str) -> Vec<&FunctionRef> {
let key = (class_name.to_string(), method_name.to_string());
self.by_class_method
.get(&key)
.map(|indices| {
indices
.iter()
.map(|&idx| &self.functions[idx].func_ref)
.collect()
})
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn lookup_simple(&self, simple_module: &str, func_name: &str) -> Vec<&FunctionRef> {
let key = (simple_module.to_string(), func_name.to_string());
self.by_simple_module
.get(&key)
.map(|indices| {
indices
.iter()
.map(|&idx| &self.functions[idx].func_ref)
.collect()
})
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn get_definition(&self, qname: &str) -> Option<&FunctionDef> {
self.by_qualified
.get(qname)
.map(|&idx| &self.functions[idx])
}
#[allow(dead_code)]
pub fn all_functions(&self) -> impl Iterator<Item = &FunctionRef> {
self.functions.iter().map(|def| &def.func_ref)
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.functions.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.functions.is_empty()
}
#[allow(dead_code)]
pub fn files(&self) -> impl Iterator<Item = &String> {
self.by_file.keys()
}
#[allow(dead_code)]
pub fn contains(&self, name: &str) -> bool {
self.by_name.contains_key(name)
}
#[allow(dead_code)]
pub fn statistics(&self) -> &IndexStats {
&self.stats
}
#[allow(dead_code)]
pub fn iter(&self) -> impl Iterator<Item = &FunctionDef> {
self.functions.iter()
}
}
fn collect_nested_class_ids<'a>(
classes: &'a [crate::ast::types::ClassInfo],
) -> std::collections::HashSet<(&'a str, usize)> {
let mut nested_ids = std::collections::HashSet::new();
fn collect_inner<'a>(
class: &'a crate::ast::types::ClassInfo,
result: &mut std::collections::HashSet<(&'a str, usize)>,
) {
for inner in &class.inner_classes {
result.insert((inner.name.as_str(), inner.line_number));
collect_inner(inner, result);
}
}
for class in classes {
collect_inner(class, &mut nested_ids);
}
nested_ids
}
fn collect_all_method_identities<'a>(
classes: &'a [crate::ast::types::ClassInfo],
) -> std::collections::HashSet<(&'a str, usize)> {
let mut method_ids = std::collections::HashSet::new();
fn collect_from_class<'a>(
class: &'a crate::ast::types::ClassInfo,
result: &mut std::collections::HashSet<(&'a str, usize)>,
) {
for method in &class.methods {
result.insert((method.name.as_str(), method.line_number));
}
for inner in &class.inner_classes {
collect_from_class(inner, result);
}
}
for class in classes {
collect_from_class(class, &mut method_ids);
}
method_ids
}
fn extract_functions_from_file(path: &PathBuf, root: Option<&Path>) -> Result<FileExtraction> {
let module_info = AstExtractor::extract_file(path)?;
let rel_path = if let Some(root) = root {
path.strip_prefix(root)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| path.clone())
} else {
path.clone()
};
let file_str = path.display().to_string();
let mut functions = Vec::new();
let module_name = if module_info.language == "go" {
get_go_module_name(path, None)
} else {
compute_module_name(&rel_path, &module_info.language)
};
let simple_module = path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string());
let method_identities: std::collections::HashSet<(&str, usize)> =
collect_all_method_identities(&module_info.classes);
for func in &module_info.functions {
if method_identities.contains(&(func.name.as_str(), func.line_number)) {
continue;
}
let qname = build_qualified_name(&module_name, None, &func.name, &module_info.language);
functions.push(FunctionDef {
func_ref: FunctionRef {
file: file_str.clone(),
name: func.name.clone(),
qualified_name: Some(qname),
},
is_method: false, class_name: None,
line_number: func.line_number,
language: module_info.language.clone(),
simple_module: simple_module.clone(),
});
}
let nested_class_ids: std::collections::HashSet<(&str, usize)> =
collect_nested_class_ids(&module_info.classes);
for class in &module_info.classes {
if nested_class_ids.contains(&(class.name.as_str(), class.line_number)) {
continue;
}
let is_nested_by_decorator = class.decorators.iter().any(|d| d.starts_with("nested_in:"));
if is_nested_by_decorator {
continue;
}
index_class_recursive(
class,
None, &module_name,
&file_str,
&module_info.language,
&simple_module,
&mut functions,
);
}
Ok(FileExtraction { functions })
}
fn index_class_recursive(
class: &crate::ast::types::ClassInfo,
parent_class_path: Option<&str>,
module_name: &str,
file_str: &str,
language: &str,
simple_module: &Option<String>,
functions: &mut Vec<FunctionDef>,
) {
let full_class_path = match parent_class_path {
Some(parent) => format!("{}.{}", parent, class.name),
None => class.name.clone(),
};
for method in &class.methods {
let qname = build_qualified_name(module_name, Some(&full_class_path), &method.name, language);
functions.push(FunctionDef {
func_ref: FunctionRef {
file: file_str.to_string(),
name: method.name.clone(),
qualified_name: Some(qname),
},
is_method: true,
class_name: Some(full_class_path.clone()),
line_number: method.line_number,
language: language.to_string(),
simple_module: simple_module.clone(),
});
}
let class_qname = build_qualified_name(module_name, parent_class_path, &class.name, language);
functions.push(FunctionDef {
func_ref: FunctionRef {
file: file_str.to_string(),
name: class.name.clone(),
qualified_name: Some(class_qname),
},
is_method: false,
class_name: parent_class_path.map(|s| s.to_string()),
line_number: class.line_number,
language: language.to_string(),
simple_module: simple_module.clone(),
});
for inner_class in &class.inner_classes {
index_class_recursive(
inner_class,
Some(&full_class_path),
module_name,
file_str,
language,
simple_module,
functions,
);
}
}
fn extract_go_package_name(source: &[u8]) -> Option<String> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.ok()?;
let tree = parser.parse(source, None)?;
let root = tree.root_node();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "package_clause" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "package_identifier" {
let name = inner_child.utf8_text(source).ok()?.to_string();
return Some(name);
}
}
}
}
None
}
fn get_go_module_name(path: &Path, source: Option<&[u8]>) -> String {
let package_name = match source {
Some(src) => extract_go_package_name(src),
None => {
std::fs::read(path)
.ok()
.and_then(|bytes| extract_go_package_name(&bytes))
}
};
if let Some(name) = package_name {
return name;
}
path.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("main")
.to_string()
}
fn compute_module_name(path: &Path, language: &str) -> String {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let parent_parts: Vec<&str> = path
.parent()
.map(|p| p.iter().filter_map(|c| c.to_str()).collect())
.unwrap_or_default();
match language {
"python" => {
if parent_parts.is_empty() {
stem.to_string()
} else {
format!("{}.{}", parent_parts.join("."), stem)
}
}
"typescript" | "javascript" => {
if parent_parts.is_empty() {
stem.to_string()
} else {
format!("{}/{}", parent_parts.join("/"), stem)
}
}
"go" => {
get_go_module_name(path, None)
}
"rust" => {
if parent_parts.is_empty() {
stem.to_string()
} else {
format!("{}::{}", parent_parts.join("::"), stem)
}
}
"java" => {
if parent_parts.is_empty() {
stem.to_string()
} else {
format!("{}.{}", parent_parts.join("."), stem)
}
}
"c" => {
stem.to_string()
}
_ => stem.to_string(),
}
}
#[inline]
fn build_qualified_name(module: &str, class: Option<&str>, name: &str, language: &str) -> String {
let (module_sep, class_sep) = match language {
"typescript" | "javascript" => ("/", "."),
"rust" | "c" => ("::", "::"),
_ => (".", "."), };
let capacity = module.len()
+ module_sep.len()
+ class.map(|c| c.len() + class_sep.len()).unwrap_or(0)
+ name.len();
let mut result = String::with_capacity(capacity);
result.push_str(module);
if let Some(c) = class {
result.push_str(module_sep);
result.push_str(c);
result.push_str(class_sep);
result.push_str(name);
} else {
result.push_str(module_sep);
result.push_str(name);
}
result
}
#[inline]
fn build_simple_qualified_name(simple_module: &str, name: &str, language: &str) -> String {
let sep = match language {
"rust" | "c" => "::",
"typescript" | "javascript" => "/",
_ => ".",
};
let capacity = simple_module.len() + sep.len() + name.len();
let mut result = String::with_capacity(capacity);
result.push_str(simple_module);
result.push_str(sep);
result.push_str(name);
result
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes()).unwrap();
path
}
#[test]
fn test_build_index_python() {
let dir = TempDir::new().unwrap();
let content = r#"
def standalone():
pass
class MyClass:
def method(self):
pass
async def async_func():
pass
"#;
let file = create_temp_file(&dir, "module.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
assert!(index.stats.files_processed >= 1);
assert!(index.len() >= 3);
let funcs = index.lookup("standalone");
assert!(!funcs.is_empty());
let methods = index.lookup("method");
assert!(!methods.is_empty());
}
#[test]
fn test_build_index_typescript() {
let dir = TempDir::new().unwrap();
let content = r#"
function greet(name: string): void {
console.log(name);
}
class Service {
handle(): void {}
}
const arrow = () => {};
"#;
let file = create_temp_file(&dir, "api.ts", content);
let index = FunctionIndex::build(&[file]).unwrap();
assert!(!index.is_empty());
let greet = index.lookup("greet");
assert!(!greet.is_empty());
}
#[test]
fn test_lookup_qualified() {
let dir = TempDir::new().unwrap();
let content = r#"
class Controller:
def handle(self):
pass
"#;
let file = create_temp_file(&dir, "web.py", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_qualified("web.Controller.handle");
assert!(result.is_some());
}
#[test]
fn test_lookup_in_file() {
let dir = TempDir::new().unwrap();
let content = r#"
def helper():
pass
def main():
helper()
"#;
let file = create_temp_file(&dir, "script.py", content);
let file_str = file.display().to_string();
let index = FunctionIndex::build(&[file]).unwrap();
let result = index.lookup_in_file(&file_str, "helper");
assert!(result.is_some());
assert_eq!(result.unwrap().name, "helper");
}
#[test]
fn test_lookup_method() {
let dir = TempDir::new().unwrap();
let content = r#"
class Service:
def process(self):
pass
class Handler:
def process(self):
pass
"#;
let file = create_temp_file(&dir, "handlers.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
let all_process = index.lookup("process");
assert_eq!(all_process.len(), 2);
let service_process = index.lookup_method("Service", "process");
assert_eq!(service_process.len(), 1);
let handler_process = index.lookup_method("Handler", "process");
assert_eq!(handler_process.len(), 1);
}
#[test]
fn test_multiple_files_same_function_name() {
let dir = TempDir::new().unwrap();
let content1 = "def helper(): pass";
let content2 = "def helper(): pass";
let file1 = create_temp_file(&dir, "module1.py", content1);
let file2 = create_temp_file(&dir, "module2.py", content2);
let index = FunctionIndex::build(&[file1, file2]).unwrap();
let helpers = index.lookup("helper");
assert_eq!(helpers.len(), 2);
}
#[test]
fn test_compute_module_name_python() {
let path = Path::new("pkg/subpkg/module.py");
let module = compute_module_name(path, "python");
assert_eq!(module, "pkg.subpkg.module");
}
#[test]
fn test_compute_module_name_typescript() {
let path = Path::new("src/utils/helpers.ts");
let module = compute_module_name(path, "typescript");
assert_eq!(module, "src/utils/helpers");
}
#[test]
fn test_compute_module_name_rust() {
let path = Path::new("src/lib/parser.rs");
let module = compute_module_name(path, "rust");
assert_eq!(module, "src::lib::parser");
}
#[test]
fn test_build_qualified_name_python() {
let qname = build_qualified_name("module", Some("Class"), "method", "python");
assert_eq!(qname, "module.Class.method");
let qname = build_qualified_name("module", None, "func", "python");
assert_eq!(qname, "module.func");
}
#[test]
fn test_build_qualified_name_typescript() {
let qname = build_qualified_name("utils", Some("Helper"), "run", "typescript");
assert_eq!(qname, "utils/Helper.run");
let qname = build_qualified_name("utils", None, "parse", "typescript");
assert_eq!(qname, "utils/parse");
}
#[test]
fn test_build_qualified_name_rust() {
let qname = build_qualified_name("parser", Some("Lexer"), "tokenize", "rust");
assert_eq!(qname, "parser::Lexer::tokenize");
let qname = build_qualified_name("utils", None, "helper", "rust");
assert_eq!(qname, "utils::helper");
}
#[test]
fn test_empty_index() {
let index = FunctionIndex::default();
assert!(index.is_empty());
assert_eq!(index.len(), 0);
assert!(index.lookup("anything").is_empty());
assert!(index.lookup_qualified("any.thing").is_none());
}
#[test]
fn test_class_indexed_for_constructors() {
let dir = TempDir::new().unwrap();
let content = r#"
class MyService:
def __init__(self):
pass
"#;
let file = create_temp_file(&dir, "service.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
let classes = index.lookup("MyService");
assert!(!classes.is_empty());
}
#[test]
fn test_deduplication_uses_name_and_line_not_just_line() {
let dir = TempDir::new().unwrap();
let content = r#"
def helper():
"""Standalone function"""
pass
class MyClass:
def process(self):
"""Method with unique name"""
pass
def process_data():
"""Another standalone function"""
pass
"#;
let file = create_temp_file(&dir, "module.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
let helpers = index.lookup("helper");
assert_eq!(helpers.len(), 1, "helper function should be indexed");
let processes = index.lookup("process");
assert_eq!(processes.len(), 1, "process method should be indexed");
let process_datas = index.lookup("process_data");
assert_eq!(
process_datas.len(),
1,
"process_data function should be indexed"
);
let method = index.lookup_method("MyClass", "process");
assert_eq!(
method.len(),
1,
"MyClass.process method should be found via method lookup"
);
}
#[test]
fn test_deduplication_skips_method_appearing_in_functions_list() {
let dir = TempDir::new().unwrap();
let content = r#"
class Controller:
def handle(self):
pass
def process(self):
pass
"#;
let file = create_temp_file(&dir, "api.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
let handles = index.lookup("handle");
assert_eq!(
handles.len(),
1,
"handle should appear exactly once (not duplicated)"
);
let processes = index.lookup("process");
assert_eq!(
processes.len(),
1,
"process should appear exactly once (not duplicated)"
);
assert_eq!(index.lookup_method("Controller", "handle").len(), 1);
assert_eq!(index.lookup_method("Controller", "process").len(), 1);
}
#[test]
fn test_simple_module_lookup() {
let dir = TempDir::new().unwrap();
let content = r#"
def helper():
pass
class Service:
def process(self):
pass
"#;
let file = create_temp_file(&dir, "pkg/subpkg/utils.py", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_qualified("pkg.subpkg.utils.helper");
assert!(result.is_some(), "full qualified lookup should work");
let result = index.lookup_simple("utils", "helper");
assert!(
!result.is_empty(),
"simple module lookup (utils, helper) should work"
);
let result = index.lookup_simple("utils", "process");
assert!(
!result.is_empty(),
"simple module lookup for method should work"
);
}
#[test]
fn test_simple_module_typescript() {
let dir = TempDir::new().unwrap();
let content = r#"
function greet(name: string): void {
console.log(name);
}
"#;
let file = create_temp_file(&dir, "src/utils/helpers.ts", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_simple("helpers", "greet");
assert!(
!result.is_empty(),
"TypeScript simple module lookup should work"
);
}
#[test]
fn test_build_simple_qualified_name_helper() {
assert_eq!(
build_simple_qualified_name("module", "func", "python"),
"module.func"
);
assert_eq!(
build_simple_qualified_name("helpers", "greet", "typescript"),
"helpers/greet"
);
assert_eq!(
build_simple_qualified_name("parser", "tokenize", "rust"),
"parser::tokenize"
);
}
#[test]
fn test_extract_go_package_name_main() {
let source = br#"
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
"#;
let result = extract_go_package_name(source);
assert_eq!(result, Some("main".to_string()));
}
#[test]
fn test_extract_go_package_name_utils() {
let source = br#"
package utils
func Helper() string {
return "helper"
}
"#;
let result = extract_go_package_name(source);
assert_eq!(result, Some("utils".to_string()));
}
#[test]
fn test_extract_go_package_name_with_comment() {
let source = br#"
// Package myserver implements a web server.
package myserver
import "net/http"
func Serve() {
}
"#;
let result = extract_go_package_name(source);
assert_eq!(result, Some("myserver".to_string()));
}
#[test]
fn test_extract_go_package_name_invalid_source() {
let source = b"this is not valid go code";
let result = extract_go_package_name(source);
assert_eq!(result, None);
}
#[test]
fn test_go_module_name_uses_package_not_directory() {
let dir = TempDir::new().unwrap();
let content = r#"
package main
func Run() string {
return "running"
}
"#;
let file = create_temp_file(&dir, "cmd/myapp/main.go", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_qualified("main.Run");
assert!(
result.is_some(),
"Function should be qualified as main.Run (from package declaration)"
);
let wrong_result = index.lookup_qualified("myapp.Run");
assert!(
wrong_result.is_none(),
"Function should NOT be qualified as myapp.Run (directory name)"
);
}
#[test]
fn test_go_package_utils_not_internal_utils() {
let dir = TempDir::new().unwrap();
let content = r#"
package utils
func Helper() string {
return "helper"
}
"#;
let file = create_temp_file(&dir, "internal/utils/helper.go", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_qualified("utils.Helper");
assert!(
result.is_some(),
"Function should be qualified as utils.Helper (from package declaration)"
);
}
#[test]
fn test_is_method_class_name_invariant() {
let dir = TempDir::new().unwrap();
let content = r#"
def standalone_with_self(self, data):
"""A standalone function that happens to have a 'self' parameter.
This should NOT be marked as a method because it's not inside a class."""
return data
def normal_function():
"""A normal function without self parameter."""
pass
class MyClass:
def actual_method(self):
"""This IS a method inside a class."""
pass
"#;
let file = create_temp_file(&dir, "module.py", content);
let index = FunctionIndex::build(&[file]).unwrap();
for def in index.iter() {
if def.is_method {
assert!(
def.class_name.is_some(),
"INVARIANT VIOLATION: {} has is_method=true but class_name=None",
def.func_ref.qualified_name.as_deref().unwrap_or(&def.func_ref.name)
);
}
}
let standalone = index.lookup("standalone_with_self");
assert_eq!(standalone.len(), 1, "Should find standalone_with_self");
let qname = standalone[0].qualified_name.as_ref().unwrap();
let def = index.get_definition(qname).unwrap();
assert!(
!def.is_method,
"standalone_with_self should NOT be marked as a method even though it has 'self' param"
);
assert!(
def.class_name.is_none(),
"standalone_with_self should not have a class_name"
);
let actual_method = index.lookup_method("MyClass", "actual_method");
assert_eq!(actual_method.len(), 1, "Should find MyClass.actual_method");
let method_qname = actual_method[0].qualified_name.as_ref().unwrap();
let method_def = index.get_definition(method_qname).unwrap();
assert!(
method_def.is_method,
"actual_method should be marked as a method"
);
assert_eq!(
method_def.class_name,
Some("MyClass".to_string()),
"actual_method should have class_name = MyClass"
);
}
#[test]
fn test_go_package_with_method() {
let dir = TempDir::new().unwrap();
let content = r#"
package myservice
type Service struct {}
func (s *Service) Run() string {
return "running"
}
func NewService() *Service {
return &Service{}
}
"#;
let file = create_temp_file(&dir, "pkg/server/service.go", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let result = index.lookup_qualified("myservice.NewService");
assert!(
result.is_some(),
"Function should be qualified as myservice.NewService"
);
let method_result = index.lookup_qualified("myservice.Run");
assert!(
method_result.is_some(),
"Receiver method should be qualified as myservice.Run"
);
let methods = index.lookup("Run");
assert!(
!methods.is_empty(),
"Receiver method Run should be found by simple name"
);
let run_def = index.get_definition("myservice.Run");
assert!(run_def.is_some(), "Should have definition for myservice.Run");
let def = run_def.unwrap();
assert!(
!def.is_method || def.class_name.is_some(),
"INVARIANT: is_method=true requires class_name to be set"
);
assert!(
def.class_name.is_none(),
"Go receiver methods don't have class_name (receiver is in decorators)"
);
assert!(
!def.is_method,
"Go receiver methods indexed without class_name should have is_method=false"
);
}
#[test]
fn test_nested_class_qualified_names() {
let dir = TempDir::new().unwrap();
let content = r#"
class Outer:
def outer_method(self):
pass
class Middle:
def middle_method(self):
pass
class Inner:
def deep_method(self):
pass
"#;
let file = create_temp_file(&dir, "nested.py", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let outer = index.lookup_qualified("nested.Outer");
assert!(outer.is_some(), "Should find Outer class as nested.Outer");
let middle = index.lookup_qualified("nested.Outer.Middle");
assert!(
middle.is_some(),
"Should find Middle class as nested.Outer.Middle (not just nested.Middle)"
);
let inner = index.lookup_qualified("nested.Outer.Middle.Inner");
assert!(
inner.is_some(),
"Should find Inner class as nested.Outer.Middle.Inner (not just nested.Inner)"
);
let deep_method = index.lookup_qualified("nested.Outer.Middle.Inner.deep_method");
assert!(
deep_method.is_some(),
"Should find deep_method as nested.Outer.Middle.Inner.deep_method"
);
let method_def = index.get_definition("nested.Outer.Middle.Inner.deep_method");
assert!(
method_def.is_some(),
"Should have definition for deep_method"
);
let def = method_def.unwrap();
assert!(def.is_method, "deep_method should be marked as a method");
assert_eq!(
def.class_name,
Some("Outer.Middle.Inner".to_string()),
"deep_method's class_name should be the full nested path"
);
let outer_method = index.lookup_qualified("nested.Outer.outer_method");
assert!(
outer_method.is_some(),
"Should find outer_method as nested.Outer.outer_method"
);
let middle_method = index.lookup_qualified("nested.Outer.Middle.middle_method");
assert!(
middle_method.is_some(),
"Should find middle_method as nested.Outer.Middle.middle_method"
);
let wrong_inner = index.lookup_qualified("nested.Inner");
assert!(
wrong_inner.is_none(),
"Should NOT find Inner as nested.Inner (missing parent class path)"
);
let wrong_method = index.lookup_qualified("nested.Inner.deep_method");
assert!(
wrong_method.is_none(),
"Should NOT find deep_method as nested.Inner.deep_method"
);
}
#[test]
fn test_nested_class_method_lookup() {
let dir = TempDir::new().unwrap();
let content = r#"
class Parent:
class Child:
def child_method(self):
pass
"#;
let file = create_temp_file(&dir, "hierarchy.py", content);
let index = FunctionIndex::build_with_root(&[file], Some(dir.path())).unwrap();
let found = index.lookup_method("Parent.Child", "child_method");
assert!(
!found.is_empty(),
"Should find child_method with class_name='Parent.Child'"
);
let not_found = index.lookup_method("Child", "child_method");
assert!(
not_found.is_empty(),
"Should NOT find child_method with class_name='Child' (needs full path)"
);
}
}