1use 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#[derive(Debug, Clone)]
31pub struct FunctionDef {
32 pub func_ref: FunctionRef,
35 pub is_method: bool,
37 pub class_name: Option<String>,
39 pub line_number: usize,
41 pub language: String,
43 pub simple_module: Option<String>,
48}
49
50#[derive(Debug, Default)]
66pub struct FunctionIndex {
67 functions: Vec<FunctionDef>,
70
71 by_name: HashMap<String, Vec<usize>>,
74
75 by_qualified: HashMap<String, usize>,
83
84 by_file: HashMap<String, Vec<usize>>,
87
88 by_class_method: HashMap<(String, String), Vec<usize>>,
94
95 by_simple_module: HashMap<(String, String), Vec<usize>>,
107
108 pub stats: IndexStats,
110}
111
112#[derive(Debug, Default, Clone)]
114pub struct IndexStats {
115 pub files_processed: usize,
117 pub parse_errors: usize,
119 pub functions_indexed: usize,
121 pub methods_indexed: usize,
123}
124
125struct FileExtraction {
127 functions: Vec<FunctionDef>,
128}
129
130impl FunctionIndex {
131 pub fn build(files: &[PathBuf]) -> Result<Self> {
141 Self::build_with_root(files, None)
142 }
143
144 pub fn build_with_root(files: &[PathBuf], root: Option<&Path>) -> Result<Self> {
153 let mut index = Self::default();
154
155 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 None
164 }
165 }
166 })
167 .collect();
168
169 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 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 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 if func_def.is_method {
201 self.stats.methods_indexed += 1;
202 } else {
203 self.stats.functions_indexed += 1;
204 }
205
206 self.by_name.entry(name.clone()).or_default().push(idx);
208
209 if let Some(ref qname) = func_def.func_ref.qualified_name {
211 self.by_qualified.insert(qname.clone(), idx);
212 }
213
214 if let Some(ref simple_module) = func_def.simple_module {
216 let key = (simple_module.clone(), name.clone());
218 self.by_simple_module.entry(key).or_default().push(idx);
219
220 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 if func_def.func_ref.qualified_name.as_ref() != Some(&simple_qname) {
244 self.by_qualified.entry(simple_qname).or_insert(idx);
246 }
247 }
248 }
249
250 self.by_file.entry(file).or_default().push(idx);
252
253 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 self.functions.push(func_def);
263 }
264
265 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 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 #[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 #[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 #[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 #[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 #[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 #[allow(dead_code)]
408 pub fn len(&self) -> usize {
409 self.functions.len()
410 }
411
412 #[allow(dead_code)]
414 pub fn is_empty(&self) -> bool {
415 self.functions.is_empty()
416 }
417
418 #[allow(dead_code)]
420 pub fn files(&self) -> impl Iterator<Item = &String> {
421 self.by_file.keys()
422 }
423
424 #[allow(dead_code)]
426 pub fn contains(&self, name: &str) -> bool {
427 self.by_name.contains_key(name)
428 }
429
430 #[allow(dead_code)]
432 pub fn statistics(&self) -> &IndexStats {
433 &self.stats
434 }
435
436 #[allow(dead_code)]
440 pub fn iter(&self) -> impl Iterator<Item = &FunctionDef> {
441 self.functions.iter()
442 }
443}
444
445fn 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
476fn 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 for method in &class.methods {
494 result.insert((method.name.as_str(), method.line_number));
495 }
496 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
509fn extract_functions_from_file(path: &PathBuf, root: Option<&Path>) -> Result<FileExtraction> {
514 let module_info = AstExtractor::extract_file(path)?;
515
516 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 let module_name = if module_info.language == "go" {
532 get_go_module_name(path, None)
534 } else {
535 compute_module_name(&rel_path, &module_info.language)
536 };
537
538 let simple_module = path
541 .file_stem()
542 .and_then(|s| s.to_str())
543 .map(|s| s.to_string());
544
545 let method_identities: std::collections::HashSet<(&str, usize)> =
562 collect_all_method_identities(&module_info.classes);
563
564 for func in &module_info.functions {
566 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 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, 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 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 if nested_class_ids.contains(&(class.name.as_str(), class.line_number)) {
609 continue;
610 }
611
612 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, &module_name,
622 &file_str,
623 &module_info.language,
624 &simple_module,
625 &mut functions,
626 );
627 }
628
629 Ok(FileExtraction { functions })
630}
631
632fn 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 let full_class_path = match parent_class_path {
669 Some(parent) => format!("{}.{}", parent, class.name),
670 None => class.name.clone(),
671 };
672
673 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 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 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
721fn 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 let mut cursor = root.walk();
747 for child in root.children(&mut cursor) {
748 if child.kind() == "package_clause" {
749 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
763fn get_go_module_name(path: &Path, source: Option<&[u8]>) -> String {
775 let package_name = match source {
777 Some(src) => extract_go_package_name(src),
778 None => {
779 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 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
798fn 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 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 if parent_parts.is_empty() {
850 stem.to_string()
851 } else {
852 format!("{}.{}", parent_parts.join("."), stem)
853 }
854 }
855 "c" => {
856 stem.to_string()
858 }
859 _ => stem.to_string(),
860 }
861}
862
863#[inline]
875fn build_qualified_name(module: &str, class: Option<&str>, name: &str, language: &str) -> String {
876 let (module_sep, class_sep) = match language {
881 "typescript" | "javascript" => ("/", "."),
882 "rust" | "c" => ("::", "::"),
883 _ => (".", "."), };
885
886 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#[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 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 assert!(index.stats.files_processed >= 1);
978 assert!(index.len() >= 3); 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 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 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 let all_process = index.lookup("process");
1072 assert_eq!(all_process.len(), 2);
1073
1074 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 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 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 let dir = TempDir::new().unwrap();
1182
1183 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 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 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 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 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 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 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 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 let result = index.lookup_qualified("pkg.subpkg.utils.helper");
1288 assert!(result.is_some(), "full qualified lookup should work");
1289
1290 let result = index.lookup_simple("utils", "helper");
1292 assert!(
1293 !result.is_empty(),
1294 "simple module lookup (utils, helper) should work"
1295 );
1296
1297 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 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 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 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 #[test]
1349 fn test_extract_go_package_name_main() {
1350 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 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 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 let source = b"this is not valid go code";
1398 let result = extract_go_package_name(source);
1399 assert_eq!(result, None);
1401 }
1402
1403 #[test]
1404 fn test_go_module_name_uses_package_not_directory() {
1405 let dir = TempDir::new().unwrap();
1407
1408 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 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 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 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 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 let dir = TempDir::new().unwrap();
1466
1467 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 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 let standalone = index.lookup("standalone_with_self");
1500 assert_eq!(standalone.len(), 1, "Should find standalone_with_self");
1501
1502 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 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 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 let result = index.lookup_qualified("myservice.NewService");
1554 assert!(
1555 result.is_some(),
1556 "Function should be qualified as myservice.NewService"
1557 );
1558
1559 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 let methods = index.lookup("Run");
1570 assert!(
1571 !methods.is_empty(),
1572 "Receiver method Run should be found by simple name"
1573 );
1574
1575 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 assert!(
1587 !def.is_method || def.class_name.is_some(),
1588 "INVARIANT: is_method=true requires class_name to be set"
1589 );
1590
1591 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 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 let outer = index.lookup_qualified("nested.Outer");
1638 assert!(outer.is_some(), "Should find Outer class as nested.Outer");
1639
1640 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 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 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 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 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 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 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 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 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 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}