1use std::path::{Path, PathBuf};
4
5use tree_sitter::Language;
6
7use astmap_core::SymbolKind;
8
9use super::{
10 collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
11 symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
12 LanguageResolver, LanguageSupport, ParseResult,
13};
14
15pub(super) fn lang() -> LanguageSupport {
16 LanguageSupport {
17 name: "rust",
18 extensions: &["rs"],
19 parser: &RustParser,
20 resolver_factory: |_root| Box::new(RustResolver),
21 config_files: &[],
22 sibling_fn: rust_sibling_expansion,
23 }
24}
25
26fn rust_sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
27 let stem = rel_path.strip_suffix(".rs")?;
28 Some(astmap_core::SiblingExpansion {
29 prefix: format!("{}/", stem),
30 extension: String::new(), root_only: false,
32 })
33}
34
35pub(crate) struct RustParser;
38
39impl LanguageParser for RustParser {
40 fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
41 parse_rust_file(source)
42 }
43
44 fn language_name(&self) -> &str {
45 "rust"
46 }
47}
48
49fn rust_language() -> Language {
50 tree_sitter_rust::LANGUAGE.into()
51}
52
53fn parse_rust_file(source: &str) -> ParseResult {
54 parse_with_tree_sitter(
55 source,
56 &rust_language(),
57 |root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
58 extract_calls,
59 |root, src, symbols| {
60 collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_builtin_type)
61 },
62 )
63}
64
65fn extract_from_node(
66 node: tree_sitter::Node,
67 source: &str,
68 symbols: &mut Vec<ExtractedSymbol>,
69 imports: &mut Vec<ExtractedImport>,
70 parent_index: Option<usize>,
71) {
72 let kind = node.kind();
73
74 match kind {
75 "struct_item" | "enum_item" | "trait_item" | "impl_item" | "type_item" => {
76 if let Some(sym) = extract_symbol(&node, source, kind, parent_index) {
77 let idx = symbols.len();
78 symbols.push(sym);
79 for i in 0..node.child_count() {
80 if let Some(child) = node.child(i as u32) {
81 extract_from_node(child, source, symbols, imports, Some(idx));
82 }
83 }
84 return;
85 }
86 }
87 "function_item" => {
88 if let Some(sym) = extract_function(&node, source, parent_index) {
89 symbols.push(sym);
90 return;
91 }
92 }
93 "function_signature_item" => {
94 if let Some(sym) = extract_function(&node, source, parent_index) {
95 symbols.push(sym);
96 return;
97 }
98 }
99 "mod_item" => {
100 if let Some(sym) = extract_mod(&node, source, parent_index) {
101 let idx = symbols.len();
102 symbols.push(sym);
103 for i in 0..node.child_count() {
104 if let Some(child) = node.child(i as u32) {
105 extract_from_node(child, source, symbols, imports, Some(idx));
106 }
107 }
108 return;
109 }
110 }
111 "use_declaration" => {
112 if let Some(imp) = extract_use(&node, source) {
113 imports.push(imp);
114 }
115 }
116 _ => {}
117 }
118
119 for i in 0..node.child_count() {
120 if let Some(child) = node.child(i as u32) {
121 extract_from_node(child, source, symbols, imports, parent_index);
122 }
123 }
124}
125
126fn extract_symbol(
127 node: &tree_sitter::Node,
128 source: &str,
129 node_kind: &str,
130 parent_index: Option<usize>,
131) -> Option<ExtractedSymbol> {
132 let kind = match node_kind {
133 "struct_item" => SymbolKind::Struct,
134 "enum_item" => SymbolKind::Enum,
135 "trait_item" => SymbolKind::Trait,
136 "impl_item" => {
137 let type_node = node.child_by_field_name("type")?;
138 let type_name = node_text(&type_node, source);
139 return Some(symbol_from_node(
140 type_name,
141 SymbolKind::Impl,
142 node,
143 source,
144 parent_index,
145 ));
146 }
147 "type_item" => SymbolKind::TypeAlias,
148 _ => return None,
149 };
150
151 let name_node = node.child_by_field_name("name")?;
152 let name = node_text(&name_node, source);
153 Some(symbol_from_node(name, kind, node, source, parent_index))
154}
155
156fn extract_function(
157 node: &tree_sitter::Node,
158 source: &str,
159 parent_index: Option<usize>,
160) -> Option<ExtractedSymbol> {
161 let name_node = node.child_by_field_name("name")?;
162 let name = node_text(&name_node, source);
163 let kind = if parent_index.is_some() {
164 SymbolKind::Method
165 } else {
166 SymbolKind::Function
167 };
168 Some(symbol_from_node(name, kind, node, source, parent_index))
169}
170
171fn extract_mod(
172 node: &tree_sitter::Node,
173 source: &str,
174 parent_index: Option<usize>,
175) -> Option<ExtractedSymbol> {
176 let name_node = node.child_by_field_name("name")?;
177 let name = node_text(&name_node, source);
178 Some(symbol_from_node(
179 name,
180 SymbolKind::Module,
181 node,
182 source,
183 parent_index,
184 ))
185}
186
187fn extract_use(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
188 let text = node_text(node, source);
189 let path = text
190 .strip_prefix("use ")
191 .unwrap_or(&text)
192 .trim_end_matches(';')
193 .trim()
194 .to_string();
195
196 let imported_symbols = extract_imported_names_from_use(&path);
197
198 Some(ExtractedImport {
199 path,
200 imported_symbols,
201 })
202}
203
204fn extract_imported_names_from_use(path: &str) -> Vec<String> {
209 if let Some(brace_start) = path.find('{') {
211 if let Some(brace_end) = path.rfind('}') {
212 let inner = &path[brace_start + 1..brace_end];
213 return inner
214 .split(',')
215 .map(|s| resolve_use_alias(s.trim()))
216 .filter(|s| !s.is_empty() && s != "*" && s != "self")
217 .collect();
218 }
219 }
220 if path.ends_with("::*") {
222 return vec![];
223 }
224 if let Some(last) = path.rsplit("::").next() {
226 let name = resolve_use_alias(last.trim());
227 if !name.is_empty() && name != "self" {
228 return vec![name];
229 }
230 }
231 vec![]
232}
233
234fn resolve_use_alias(item: &str) -> String {
239 if let Some(pos) = item.find(" as ") {
241 return item[pos + 4..].trim().to_string();
242 }
243 if let Some(last) = item.rsplit("::").next() {
245 return last.trim().to_string();
246 }
247 item.to_string()
248}
249
250fn extract_calls(
251 root: tree_sitter::Node,
252 source: &str,
253 symbols: &[ExtractedSymbol],
254) -> Vec<ExtractedCall> {
255 let mut calls = Vec::new();
256 collect_calls(root, source, symbols, &mut calls);
257 calls
258}
259
260fn collect_calls(
261 node: tree_sitter::Node,
262 source: &str,
263 symbols: &[ExtractedSymbol],
264 calls: &mut Vec<ExtractedCall>,
265) {
266 if node.kind() == "call_expression" {
267 if let Some(func_node) = node.child_by_field_name("function") {
268 let callee_text = node_text(&func_node, source);
269 let callee_name = callee_text.rsplit("::").next().unwrap_or(&callee_text);
270 let callee_name = callee_name.rsplit('.').next().unwrap_or(callee_name);
271
272 let call_line = node.start_position().row + 1;
273
274 if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
275 calls.push(ExtractedCall {
276 caller_name: caller.to_string(),
277 callee_name: callee_name.to_string(),
278 line: call_line,
279 });
280 }
281 }
282 }
283
284 for i in 0..node.child_count() {
285 if let Some(child) = node.child(i as u32) {
286 collect_calls(child, source, symbols, calls);
287 }
288 }
289}
290
291fn is_builtin_type(name: &str) -> bool {
292 matches!(
293 name,
294 "bool"
295 | "char"
296 | "str"
297 | "i8"
298 | "i16"
299 | "i32"
300 | "i64"
301 | "i128"
302 | "isize"
303 | "u8"
304 | "u16"
305 | "u32"
306 | "u64"
307 | "u128"
308 | "usize"
309 | "f32"
310 | "f64"
311 | "String"
312 | "Vec"
313 | "Option"
314 | "Result"
315 | "Box"
316 | "Rc"
317 | "Arc"
318 | "HashMap"
319 | "HashSet"
320 | "BTreeMap"
321 | "BTreeSet"
322 | "Path"
323 | "PathBuf"
324 | "Self"
325 )
326}
327
328pub(crate) struct RustResolver;
331
332impl LanguageResolver for RustResolver {
333 fn resolve_import(
334 &self,
335 import_path: &str,
336 source_file: &str,
337 project_root: &Path,
338 ) -> Option<PathBuf> {
339 resolve_rust_use(import_path, source_file, project_root)
340 }
341}
342
343pub fn resolve_rust_use(use_path: &str, source_file: &str, project_root: &Path) -> Option<PathBuf> {
348 let normalized = normalize_use_path(use_path);
349 let parts: Vec<&str> = normalized.split("::").collect();
350 if parts.is_empty() {
351 return None;
352 }
353
354 match parts[0] {
355 "crate" => {
356 let crate_src = find_crate_src_dir(source_file, project_root)?;
357 resolve_module_path(&parts[1..], &crate_src)
358 }
359 "super" => {
360 let source_path = project_root.join(source_file);
361 let source_dir = source_path.parent()?.to_path_buf();
362 let file_stem = source_path.file_stem()?.to_string_lossy();
363 let base = if file_stem == "mod" {
364 source_dir.parent()?.to_path_buf()
365 } else {
366 source_dir
367 };
368 resolve_module_path(&parts[1..], &base)
369 }
370 "self" => {
371 let source_dir = project_root.join(source_file).parent()?.to_path_buf();
372 resolve_module_path(&parts[1..], &source_dir)
373 }
374 _ => None,
375 }
376}
377
378fn normalize_use_path(path: &str) -> String {
379 let mut s = path.to_string();
380 if let Some(brace_pos) = s.find('{') {
381 s.truncate(brace_pos);
382 s = s.trim_end_matches("::").to_string();
383 }
384 if s.ends_with("::*") {
385 s.truncate(s.len() - 3);
386 }
387 s
388}
389
390fn find_crate_src_dir(source_file: &str, project_root: &Path) -> Option<PathBuf> {
391 let abs = project_root.join(source_file);
392 let mut current = abs.parent()?;
393 while current.starts_with(project_root) {
394 if current.file_name().is_some_and(|n| n == "src") {
395 return Some(current.to_path_buf());
396 }
397 current = current.parent()?;
398 }
399 let fallback = project_root.join("src");
400 if fallback.exists() {
401 Some(fallback)
402 } else {
403 None
404 }
405}
406
407fn resolve_module_path(parts: &[&str], base_dir: &Path) -> Option<PathBuf> {
408 if parts.is_empty() {
409 return None;
410 }
411
412 let mut current = base_dir.to_path_buf();
413 for (i, part) in parts.iter().enumerate() {
414 let as_file = current.join(format!("{}.rs", part));
415 if as_file.exists() {
416 return Some(as_file);
417 }
418 let as_mod = current.join(part).join("mod.rs");
419 if as_mod.exists() {
420 return Some(as_mod);
421 }
422
423 if i == parts.len() - 1 {
424 let current_mod = current.join("mod.rs");
425 if current_mod.exists() {
426 return Some(current_mod);
427 }
428 if let Some(dir_name) = current.file_name() {
429 let parent = current.parent()?;
430 let as_parent_file = parent.join(format!("{}.rs", dir_name.to_string_lossy()));
431 if as_parent_file.exists() {
432 return Some(as_parent_file);
433 }
434 }
435 return None;
436 }
437
438 current = current.join(part);
439 }
440 None
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use std::fs;
447
448 #[test]
451 fn test_parse_struct() {
452 let source = r#"
453pub struct MyStruct {
454 field: i32,
455}
456"#;
457 let result = parse_rust_file(source);
458 assert!(!result.has_errors);
459 assert_eq!(result.symbols.len(), 1);
460 assert_eq!(result.symbols[0].name, "MyStruct");
461 assert_eq!(result.symbols[0].kind, SymbolKind::Struct);
462 }
463
464 #[test]
465 fn test_parse_function() {
466 let source = r#"
467fn hello(name: &str) -> String {
468 format!("Hello, {}", name)
469}
470"#;
471 let result = parse_rust_file(source);
472 assert!(!result.has_errors);
473 assert_eq!(result.symbols.len(), 1);
474 assert_eq!(result.symbols[0].name, "hello");
475 assert_eq!(result.symbols[0].kind, SymbolKind::Function);
476 assert!(result.symbols[0]
477 .signature
478 .as_ref()
479 .unwrap()
480 .contains("fn hello"));
481 }
482
483 #[test]
484 fn test_parse_imports() {
485 let source = r#"
486use std::io::Read;
487use crate::db::Database;
488use super::models::Symbol;
489"#;
490 let result = parse_rust_file(source);
491 assert_eq!(result.imports.len(), 3);
492 assert_eq!(result.imports[0].path, "std::io::Read");
493 assert_eq!(result.imports[1].path, "crate::db::Database");
494 assert_eq!(result.imports[2].path, "super::models::Symbol");
495 }
496
497 #[test]
498 fn test_parse_trait_and_impl() {
499 let source = r#"
500pub trait Greet {
501 fn greet(&self) -> String;
502}
503
504pub struct Greeter;
505
506impl Greet for Greeter {
507 fn greet(&self) -> String {
508 "hello".to_string()
509 }
510}
511"#;
512 let result = parse_rust_file(source);
513 assert!(!result.has_errors);
514 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
515 assert!(names.contains(&"Greet"));
516 assert!(names.contains(&"Greeter"));
517 assert!(names.contains(&"greet"));
518 }
519
520 #[test]
521 fn test_parse_enum() {
522 let source = r#"
523pub enum Color {
524 Red,
525 Green,
526 Blue,
527}
528"#;
529 let result = parse_rust_file(source);
530 assert!(!result.has_errors);
531 assert_eq!(result.symbols.len(), 1);
532 assert_eq!(result.symbols[0].name, "Color");
533 assert_eq!(result.symbols[0].kind, SymbolKind::Enum);
534 }
535
536 #[test]
537 fn test_parse_empty_file() {
538 let result = parse_rust_file("");
539 assert!(!result.has_errors);
540 assert_eq!(result.symbols.len(), 0);
541 assert_eq!(result.imports.len(), 0);
542 }
543
544 #[test]
545 fn test_extract_calls() {
546 let source = r#"
547fn helper() -> i32 { 42 }
548
549fn main() {
550 let x = helper();
551 println!("{}", x);
552}
553"#;
554 let result = parse_rust_file(source);
555 assert!(!result.calls.is_empty());
556 let call_names: Vec<&str> = result
557 .calls
558 .iter()
559 .map(|c| c.callee_name.as_str())
560 .collect();
561 assert!(call_names.contains(&"helper"), "calls: {:?}", result.calls);
562 }
563
564 #[test]
565 fn test_extract_type_refs() {
566 let source = r#"
567struct Config {
568 name: String,
569}
570
571struct Database {
572 config: Config,
573}
574
575fn create_db(config: Config) -> Database {
576 Database { config }
577}
578"#;
579 let result = parse_rust_file(source);
580 let ref_types: Vec<&str> = result
581 .type_refs
582 .iter()
583 .map(|r| r.to_type.as_str())
584 .collect();
585 assert!(
586 ref_types.contains(&"Config"),
587 "type_refs: {:?}",
588 result.type_refs
589 );
590 assert!(
591 ref_types.contains(&"Database"),
592 "type_refs: {:?}",
593 result.type_refs
594 );
595 }
596
597 #[test]
598 fn test_method_calls() {
599 let source = r#"
600struct Foo;
601
602impl Foo {
603 fn bar(&self) -> i32 { 42 }
604 fn baz(&self) {
605 let x = self.bar();
606 }
607}
608"#;
609 let result = parse_rust_file(source);
610 let call_names: Vec<&str> = result
611 .calls
612 .iter()
613 .map(|c| c.callee_name.as_str())
614 .collect();
615 assert!(call_names.contains(&"bar"), "calls: {:?}", result.calls);
616 }
617
618 #[test]
619 fn test_parse_type_alias() {
620 let source = "type MyResult = Result<(), Box<dyn std::error::Error>>;";
621 let result = parse_rust_file(source);
622 assert!(!result.has_errors);
623 assert_eq!(result.symbols.len(), 1);
624 assert_eq!(result.symbols[0].name, "MyResult");
625 assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
626 }
627
628 #[test]
629 fn test_parse_module() {
630 let source = r#"
631mod inner {
632 fn private_fn() {}
633}
634"#;
635 let result = parse_rust_file(source);
636 assert!(!result.has_errors);
637 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
638 assert!(names.contains(&"inner"));
639 assert!(names.contains(&"private_fn"));
640 let private_fn = result
641 .symbols
642 .iter()
643 .find(|s| s.name == "private_fn")
644 .unwrap();
645 assert!(private_fn.parent_index.is_some());
646 }
647
648 #[test]
649 fn test_parse_impl_methods_extracted() {
650 let source = r#"
651struct MyStruct;
652
653impl MyStruct {
654 fn new() -> Self { MyStruct }
655}
656"#;
657 let result = parse_rust_file(source);
658 assert!(!result.has_errors);
659 let struct_sym = result
660 .symbols
661 .iter()
662 .find(|s| s.name == "MyStruct")
663 .unwrap();
664 assert_eq!(struct_sym.kind, SymbolKind::Struct);
665 let new_sym = result.symbols.iter().find(|s| s.name == "new").unwrap();
666 assert!(new_sym.kind == SymbolKind::Function || new_sym.kind == SymbolKind::Method);
667 }
668
669 #[test]
670 fn test_is_builtin_type_positive() {
671 assert!(is_builtin_type("bool"));
672 assert!(is_builtin_type("String"));
673 assert!(is_builtin_type("Vec"));
674 assert!(is_builtin_type("Option"));
675 assert!(is_builtin_type("Result"));
676 assert!(is_builtin_type("HashMap"));
677 assert!(is_builtin_type("Self"));
678 assert!(is_builtin_type("PathBuf"));
679 }
680
681 #[test]
682 fn test_is_builtin_type_negative() {
683 assert!(!is_builtin_type("MyStruct"));
684 assert!(!is_builtin_type("Database"));
685 assert!(!is_builtin_type("Scanner"));
686 }
687
688 #[test]
689 fn test_parse_multiple_use_declarations() {
690 let source = r#"
691use std::collections::HashMap;
692use crate::db::{Database, DbError};
693use super::models::*;
694"#;
695 let result = parse_rust_file(source);
696 assert_eq!(result.imports.len(), 3);
697 }
698
699 #[test]
700 fn test_parse_trait_method_signature() {
701 let source = r#"
702pub trait Repository {
703 fn open(&self) -> Result<(), Error>;
704 fn close(&self);
705}
706"#;
707 let result = parse_rust_file(source);
708 assert!(!result.has_errors);
709 let methods: Vec<&str> = result
710 .symbols
711 .iter()
712 .filter(|s| s.kind == SymbolKind::Method)
713 .map(|s| s.name.as_str())
714 .collect();
715 assert!(methods.contains(&"open"));
716 assert!(methods.contains(&"close"));
717 }
718
719 #[test]
720 fn test_path_qualified_call() {
721 let source = r#"
722fn caller() {
723 std::io::stdout();
724 Vec::new();
725}
726"#;
727 let result = parse_rust_file(source);
728 let callee_names: Vec<&str> = result
729 .calls
730 .iter()
731 .map(|c| c.callee_name.as_str())
732 .collect();
733 assert!(callee_names.contains(&"stdout") || callee_names.contains(&"new"));
734 }
735
736 #[test]
737 fn test_extract_imported_names_simple() {
738 let names = extract_imported_names_from_use("crate::db::Repository");
739 assert_eq!(names, vec!["Repository"]);
740 }
741
742 #[test]
743 fn test_extract_imported_names_group() {
744 let names = extract_imported_names_from_use("crate::db::{Database, DbError}");
745 assert_eq!(names, vec!["Database", "DbError"]);
746 }
747
748 #[test]
749 fn test_extract_imported_names_wildcard() {
750 let names = extract_imported_names_from_use("super::models::*");
751 assert!(names.is_empty());
752 }
753
754 #[test]
755 fn test_extract_imported_names_as_alias() {
756 let names = extract_imported_names_from_use("crate::db::Repository as Repo");
757 assert_eq!(names, vec!["Repo"]);
758 }
759
760 #[test]
761 fn test_extract_imported_names_group_with_alias() {
762 let names = extract_imported_names_from_use("crate::db::{Repository as Repo, DbError}");
763 assert_eq!(names, vec!["Repo", "DbError"]);
764 }
765
766 #[test]
767 fn test_extract_imported_names_self_filtered() {
768 let names = extract_imported_names_from_use("crate::db::{self, Database}");
769 assert_eq!(names, vec!["Database"]);
770 }
771
772 #[test]
773 fn test_extract_imported_names_nested_path() {
774 let names = extract_imported_names_from_use("std::collections::{HashMap, hash_map::Entry}");
775 assert_eq!(names, vec!["HashMap", "Entry"]);
776 }
777
778 #[test]
779 fn test_import_extracts_imported_symbols() {
780 let source = r#"
781use crate::db::{Database, DbError};
782use super::models::*;
783use crate::scanner::Scanner as Sc;
784"#;
785 let result = parse_rust_file(source);
786 assert_eq!(result.imports.len(), 3);
787 assert_eq!(
788 result.imports[0].imported_symbols,
789 vec!["Database", "DbError"]
790 );
791 assert!(result.imports[1].imported_symbols.is_empty()); assert_eq!(result.imports[2].imported_symbols, vec!["Sc"]);
793 }
794
795 #[test]
798 fn test_resolve_crate_path_simple() {
799 let tmp = tempfile::tempdir().unwrap();
800 let src = tmp.path().join("src");
801 fs::create_dir_all(src.join("db")).unwrap();
802 fs::write(src.join("db").join("mod.rs"), "").unwrap();
803 fs::write(src.join("lib.rs"), "").unwrap();
804
805 let result = resolve_rust_use("crate::db", "src/lib.rs", tmp.path());
806 assert!(result.is_some());
807 assert!(result.unwrap().ends_with("db/mod.rs"));
808 }
809
810 #[test]
811 fn test_resolve_crate_path_workspace_subcrate() {
812 let tmp = tempfile::tempdir().unwrap();
813 let src = tmp.path().join("my-core").join("src");
814 fs::create_dir_all(src.join("db")).unwrap();
815 fs::write(src.join("db").join("mod.rs"), "").unwrap();
816 fs::write(src.join("lib.rs"), "").unwrap();
817
818 let result = resolve_rust_use("crate::db", "my-core/src/lib.rs", tmp.path());
819 assert!(result.is_some());
820 let path = result.unwrap();
821 assert!(path.ends_with("db/mod.rs"), "got: {}", path.display());
822 }
823
824 #[test]
825 fn test_resolve_crate_nested_module() {
826 let tmp = tempfile::tempdir().unwrap();
827 let src = tmp.path().join("src");
828 fs::create_dir_all(&src).unwrap();
829 fs::write(src.join("impact.rs"), "").unwrap();
830
831 let result = resolve_rust_use("crate::impact", "src/db/mod.rs", tmp.path());
832 assert!(result.is_some());
833 assert!(result.unwrap().ends_with("impact.rs"));
834 }
835
836 #[test]
837 fn test_resolve_super() {
838 let tmp = tempfile::tempdir().unwrap();
839 let src = tmp.path().join("src");
840 fs::create_dir_all(src.join("db")).unwrap();
841 fs::write(src.join("db").join("mod.rs"), "").unwrap();
842 fs::write(src.join("db").join("models.rs"), "").unwrap();
843
844 let result = resolve_rust_use("super::models", "src/db/queries.rs", tmp.path());
845 assert!(result.is_some());
846 assert!(result.unwrap().ends_with("models.rs"));
847 }
848
849 #[test]
850 fn test_external_crate_returns_none() {
851 let tmp = tempfile::tempdir().unwrap();
852 assert_eq!(
853 resolve_rust_use("std::io::Read", "src/lib.rs", tmp.path()),
854 None
855 );
856 assert_eq!(
857 resolve_rust_use("serde::Deserialize", "src/lib.rs", tmp.path()),
858 None
859 );
860 assert_eq!(
861 resolve_rust_use("tokio::spawn", "src/lib.rs", tmp.path()),
862 None
863 );
864 }
865
866 #[test]
867 fn test_normalize_use_path_braces() {
868 assert_eq!(
869 normalize_use_path("crate::db::{Database, DbError}"),
870 "crate::db"
871 );
872 }
873
874 #[test]
875 fn test_normalize_use_path_wildcard() {
876 assert_eq!(
877 normalize_use_path("crate::db::models::*"),
878 "crate::db::models"
879 );
880 }
881
882 #[test]
883 fn test_normalize_use_path_plain() {
884 assert_eq!(
885 normalize_use_path("crate::db::Database"),
886 "crate::db::Database"
887 );
888 }
889
890 #[test]
891 fn test_resolve_self() {
892 let tmp = tempfile::tempdir().unwrap();
893 let src = tmp.path().join("src").join("db");
894 fs::create_dir_all(&src).unwrap();
895 fs::write(src.join("models.rs"), "").unwrap();
896
897 let result = resolve_rust_use("self::models", "src/db/mod.rs", tmp.path());
898 assert!(result.is_some());
899 assert!(result.unwrap().ends_with("models.rs"));
900 }
901
902 #[test]
903 fn test_resolve_crate_symbol_name() {
904 let tmp = tempfile::tempdir().unwrap();
905 let src = tmp.path().join("src");
906 fs::create_dir_all(src.join("db")).unwrap();
907 fs::write(src.join("db").join("mod.rs"), "").unwrap();
908
909 let result = resolve_rust_use("crate::db::Database", "src/lib.rs", tmp.path());
910 assert!(result.is_some());
911 assert!(
912 result.unwrap().to_string_lossy().contains("db"),
913 "should resolve to db module"
914 );
915 }
916
917 #[test]
918 fn test_resolve_empty_path() {
919 let tmp = tempfile::tempdir().unwrap();
920 let result = resolve_rust_use("", "src/lib.rs", tmp.path());
921 assert!(result.is_none());
922 }
923
924 #[test]
925 fn test_find_crate_src_dir_direct() {
926 let tmp = tempfile::tempdir().unwrap();
927 let src = tmp.path().join("src");
928 fs::create_dir_all(&src).unwrap();
929 fs::write(src.join("lib.rs"), "").unwrap();
930
931 let result = find_crate_src_dir("src/lib.rs", tmp.path());
932 assert!(result.is_some());
933 }
934
935 #[test]
936 fn test_find_crate_src_dir_fallback() {
937 let tmp = tempfile::tempdir().unwrap();
938 let config_dir = tmp.path().join("config");
939 fs::create_dir_all(&config_dir).unwrap();
940 fs::write(config_dir.join("settings.rs"), "").unwrap();
941 let src = tmp.path().join("src");
942 fs::create_dir_all(&src).unwrap();
943
944 let result = find_crate_src_dir("config/settings.rs", tmp.path());
945 assert!(result.is_some());
946 assert!(result.unwrap().ends_with("src"));
947 }
948
949 #[test]
950 fn test_find_crate_src_dir_no_src() {
951 let tmp = tempfile::tempdir().unwrap();
952 let config_dir = tmp.path().join("config");
953 fs::create_dir_all(&config_dir).unwrap();
954 fs::write(config_dir.join("settings.rs"), "").unwrap();
955
956 let result = find_crate_src_dir("config/settings.rs", tmp.path());
957 assert!(result.is_none());
958 }
959
960 #[test]
961 fn test_resolve_crate_only() {
962 let tmp = tempfile::tempdir().unwrap();
963 let src = tmp.path().join("src");
964 fs::create_dir_all(&src).unwrap();
965 fs::write(src.join("lib.rs"), "").unwrap();
966
967 let result = resolve_rust_use("crate", "src/lib.rs", tmp.path());
968 assert!(result.is_none());
969 }
970
971 #[test]
972 fn test_resolve_module_path_as_rs_file() {
973 let tmp = tempfile::tempdir().unwrap();
974 let src = tmp.path().join("src");
975 fs::create_dir_all(&src).unwrap();
976 fs::write(src.join("utils.rs"), "").unwrap();
977 fs::write(src.join("lib.rs"), "").unwrap();
978
979 let result = resolve_rust_use("crate::utils", "src/lib.rs", tmp.path());
980 assert!(result.is_some());
981 assert!(result.unwrap().ends_with("utils.rs"));
982 }
983
984 #[test]
985 fn test_resolve_deep_symbol_to_parent_rs() {
986 let tmp = tempfile::tempdir().unwrap();
987 let src = tmp.path().join("src");
988 fs::create_dir_all(&src).unwrap();
989 fs::write(src.join("utils.rs"), "").unwrap();
990 fs::write(src.join("lib.rs"), "").unwrap();
991
992 let result = resolve_rust_use("crate::utils::Helper", "src/lib.rs", tmp.path());
993 assert!(result.is_some());
994 }
995
996 #[test]
997 fn test_resolve_super_from_mod_rs() {
998 let tmp = tempfile::tempdir().unwrap();
999 let src = tmp.path().join("src");
1000 fs::create_dir_all(src.join("db")).unwrap();
1001 fs::write(src.join("db").join("mod.rs"), "").unwrap();
1002 fs::write(src.join("export.rs"), "").unwrap();
1003
1004 let result = resolve_rust_use("super::export", "src/db/mod.rs", tmp.path());
1005 assert!(result.is_some());
1006 assert!(result.unwrap().ends_with("export.rs"));
1007 }
1008
1009 #[test]
1010 fn test_resolve_super_from_new_style_module() {
1011 let tmp = tempfile::tempdir().unwrap();
1012 let src = tmp.path().join("src");
1013 fs::create_dir_all(src.join("db")).unwrap();
1014 fs::write(src.join("db.rs"), "").unwrap();
1015 fs::write(src.join("export.rs"), "").unwrap();
1016
1017 let result = resolve_rust_use("super::export", "src/db.rs", tmp.path());
1018 assert!(result.is_some());
1019 assert!(result.unwrap().ends_with("export.rs"));
1020 }
1021
1022 #[test]
1023 fn test_resolve_super_nonexistent() {
1024 let tmp = tempfile::tempdir().unwrap();
1025 let src = tmp.path().join("src");
1026 fs::create_dir_all(src.join("db")).unwrap();
1027 fs::write(src.join("db").join("queries.rs"), "").unwrap();
1028
1029 let result = resolve_rust_use("super::nonexistent", "src/db/queries.rs", tmp.path());
1030 assert!(result.is_none());
1031 }
1032
1033 #[test]
1036 fn test_parse_trait_method_signatures_without_body() {
1037 let source = r#"
1039pub trait Serializer {
1040 fn serialize(&self) -> Vec<u8>;
1041 fn deserialize(data: &[u8]) -> Self;
1042}
1043"#;
1044 let result = parse_rust_file(source);
1045 assert!(!result.has_errors);
1046 let methods: Vec<&str> = result
1047 .symbols
1048 .iter()
1049 .filter(|s| s.kind == SymbolKind::Method)
1050 .map(|s| s.name.as_str())
1051 .collect();
1052 assert!(methods.contains(&"serialize"), "methods: {:?}", methods);
1053 assert!(methods.contains(&"deserialize"), "methods: {:?}", methods);
1054 let trait_idx = result
1056 .symbols
1057 .iter()
1058 .position(|s| s.name == "Serializer")
1059 .unwrap();
1060 for sym in result
1061 .symbols
1062 .iter()
1063 .filter(|s| s.kind == SymbolKind::Method)
1064 {
1065 assert_eq!(
1066 sym.parent_index,
1067 Some(trait_idx),
1068 "method '{}' should be child of Serializer",
1069 sym.name
1070 );
1071 }
1072 }
1073
1074 #[test]
1075 fn test_parse_nested_module_with_items() {
1076 let source = r#"
1077mod outer {
1078 mod inner {
1079 fn deep_fn() {}
1080 }
1081 fn outer_fn() {}
1082}
1083"#;
1084 let result = parse_rust_file(source);
1085 assert!(!result.has_errors);
1086 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1087 assert!(names.contains(&"outer"), "symbols: {:?}", names);
1088 assert!(names.contains(&"inner"), "symbols: {:?}", names);
1089 assert!(names.contains(&"deep_fn"), "symbols: {:?}", names);
1090 assert!(names.contains(&"outer_fn"), "symbols: {:?}", names);
1091 let outer_idx = result
1093 .symbols
1094 .iter()
1095 .position(|s| s.name == "outer")
1096 .unwrap();
1097 let inner = result.symbols.iter().find(|s| s.name == "inner").unwrap();
1098 assert_eq!(inner.parent_index, Some(outer_idx));
1099 }
1100
1101 #[test]
1102 fn test_parse_impl_for_trait_methods_extracted() {
1103 let source = r#"
1106trait Render {
1107 fn render(&self) -> String;
1108 fn refresh(&self);
1109}
1110struct Widget;
1111impl Render for Widget {
1112 fn render(&self) -> String { "widget".to_string() }
1113 fn refresh(&self) {}
1114}
1115"#;
1116 let result = parse_rust_file(source);
1117 assert!(!result.has_errors);
1118 let method_names: Vec<&str> = result
1119 .symbols
1120 .iter()
1121 .filter(|s| s.name == "render" || s.name == "refresh")
1122 .map(|s| s.name.as_str())
1123 .collect();
1124 assert!(
1126 method_names.iter().filter(|&&n| n == "render").count() >= 2,
1127 "render should appear in both trait and impl: {:?}",
1128 method_names
1129 );
1130 assert!(
1131 method_names.iter().filter(|&&n| n == "refresh").count() >= 2,
1132 "refresh should appear in both trait and impl: {:?}",
1133 method_names
1134 );
1135 }
1136
1137 #[test]
1138 fn test_resolve_use_alias_plain() {
1139 assert_eq!(resolve_use_alias("Foo"), "Foo");
1141 }
1142
1143 #[test]
1144 fn test_resolve_use_alias_with_as() {
1145 assert_eq!(resolve_use_alias("Foo as Bar"), "Bar");
1146 }
1147
1148 #[test]
1149 fn test_resolve_use_alias_nested() {
1150 assert_eq!(resolve_use_alias("hash_map::Entry"), "Entry");
1151 }
1152
1153 #[test]
1154 fn test_normalize_use_path_combined_braces_and_suffix() {
1155 assert_eq!(normalize_use_path("crate::foo::{A, B}"), "crate::foo");
1157 }
1158
1159 #[test]
1160 fn test_extract_imported_names_empty_path() {
1161 let names = extract_imported_names_from_use("");
1162 assert!(names.is_empty());
1163 }
1164
1165 #[test]
1166 fn test_parse_const_and_static() {
1167 let source = r#"
1169const MAX: usize = 100;
1170static COUNTER: i32 = 0;
1171fn used_fn() {}
1172"#;
1173 let result = parse_rust_file(source);
1174 assert!(!result.has_errors);
1175 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1177 assert!(names.contains(&"used_fn"));
1178 }
1179
1180 #[test]
1181 fn test_parse_struct_with_impl_methods() {
1182 let source = r#"
1183pub struct Config {
1184 pub name: String,
1185}
1186
1187impl Config {
1188 pub fn new(name: &str) -> Self {
1189 Config { name: name.to_string() }
1190 }
1191 pub fn validate(&self) -> bool {
1192 !self.name.is_empty()
1193 }
1194}
1195"#;
1196 let result = parse_rust_file(source);
1197 assert!(!result.has_errors);
1198 let all_names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1200 assert!(
1201 all_names.contains(&"Config"),
1202 "struct Config should be extracted"
1203 );
1204 assert!(
1205 all_names.contains(&"new"),
1206 "new should be extracted: {:?}",
1207 all_names
1208 );
1209 assert!(
1210 all_names.contains(&"validate"),
1211 "validate should be extracted: {:?}",
1212 all_names
1213 );
1214 }
1215
1216 #[test]
1217 fn test_resolve_module_path_intermediate_not_dir() {
1218 let tmp = tempfile::tempdir().unwrap();
1220 let src = tmp.path().join("src");
1221 fs::create_dir_all(&src).unwrap();
1222 fs::write(src.join("deep"), "").unwrap();
1224
1225 let result = resolve_rust_use("crate::deep::nested", "src/lib.rs", tmp.path());
1226 assert!(result.is_none(), "intermediate non-directory should fail");
1227 }
1228
1229 #[test]
1230 fn test_parse_use_with_self() {
1231 let source = "use crate::db::{self, models};\n";
1232 let result = parse_rust_file(source);
1233 assert_eq!(result.imports.len(), 1);
1234 assert_eq!(result.imports[0].imported_symbols, vec!["models"]);
1236 }
1237
1238 #[test]
1239 fn test_type_ref_builtin_types_filtered() {
1240 let source = r#"
1242fn foo(a: bool, b: i32, c: u64, d: f32, e: String, f: Vec<i32>) {}
1243"#;
1244 let result = parse_rust_file(source);
1245 assert!(
1246 result.type_refs.is_empty(),
1247 "builtin types should be filtered: {:?}",
1248 result.type_refs
1249 );
1250 }
1251
1252 #[test]
1253 fn test_call_inside_nested_module() {
1254 let source = r#"
1255mod setup {
1256 fn init() {
1257 helper();
1258 }
1259 fn helper() {}
1260}
1261"#;
1262 let result = parse_rust_file(source);
1263 for call in &result.calls {
1265 assert!(
1266 !call.caller_name.is_empty(),
1267 "call to '{}' should have a caller",
1268 call.callee_name
1269 );
1270 }
1271 }
1272
1273 #[test]
1274 fn test_extract_imported_names_single_no_path_separator() {
1275 let names = extract_imported_names_from_use("Foo");
1276 assert_eq!(names, vec!["Foo"]);
1277 }
1278
1279 #[test]
1280 fn test_extract_imported_names_self_only() {
1281 let names = extract_imported_names_from_use("crate::db::self");
1282 assert!(
1283 names.is_empty(),
1284 "self-only import should return empty: {:?}",
1285 names
1286 );
1287 }
1288
1289 #[test]
1290 fn test_type_refs_from_struct_fields() {
1291 let source = r#"
1292struct Config { name: String }
1293struct App { config: Config, count: i32 }
1294"#;
1295 let result = parse_rust_file(source);
1296 let type_names: Vec<&str> = result
1297 .type_refs
1298 .iter()
1299 .map(|r| r.to_type.as_str())
1300 .collect();
1301 assert!(
1302 type_names.contains(&"Config"),
1303 "Config type ref should be found: {:?}",
1304 type_names
1305 );
1306 }
1307
1308 #[test]
1309 fn test_resolve_module_path_empty_parts() {
1310 let tmp = tempfile::tempdir().unwrap();
1311 let src = tmp.path().join("src");
1312 fs::create_dir_all(&src).unwrap();
1313 let result = resolve_module_path(&[], &src);
1314 assert!(result.is_none(), "empty parts should return None");
1315 }
1316
1317 #[test]
1318 fn test_parse_use_with_wildcard_in_group() {
1319 let source = "use crate::db::{*, DbError};\n";
1320 let result = parse_rust_file(source);
1321 assert_eq!(result.imports.len(), 1);
1322 assert_eq!(result.imports[0].imported_symbols, vec!["DbError"]);
1324 }
1325
1326 #[test]
1327 fn test_is_builtin_type_all_values() {
1328 for ty in &[
1330 "bool", "char", "str", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32",
1331 "u64", "u128", "usize", "f32", "f64", "String", "Vec", "Option", "Result", "Box", "Rc",
1332 "Arc", "HashMap", "HashSet", "BTreeMap", "BTreeSet", "Path", "PathBuf", "Self",
1333 ] {
1334 assert!(is_builtin_type(ty), "'{}' should be builtin", ty);
1335 }
1336 }
1337
1338 #[test]
1341 fn test_normalize_use_path_empty() {
1342 assert_eq!(normalize_use_path(""), "");
1343 }
1344
1345 #[test]
1346 fn test_normalize_use_path_single_ident() {
1347 assert_eq!(normalize_use_path("std"), "std");
1348 }
1349
1350 #[test]
1351 fn test_parse_multiple_impl_blocks() {
1352 let source = r#"
1353struct Widget;
1354
1355impl Widget {
1356 fn new() -> Self { Widget }
1357 fn render(&self) {}
1358}
1359
1360impl Widget {
1361 fn destroy(&self) {}
1362}
1363"#;
1364 let result = parse_rust_file(source);
1365 assert!(!result.has_errors);
1366 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1367 assert!(names.contains(&"Widget"));
1368 assert!(names.contains(&"new"));
1369 assert!(names.contains(&"render"));
1370 assert!(names.contains(&"destroy"));
1371 }
1372
1373 #[test]
1374 fn test_parse_generic_struct() {
1375 let source = r#"
1376pub struct Container<T: Clone> {
1377 inner: Vec<T>,
1378}
1379"#;
1380 let result = parse_rust_file(source);
1381 assert!(!result.has_errors);
1382 assert_eq!(result.symbols.len(), 1);
1383 assert_eq!(result.symbols[0].name, "Container");
1384 assert_eq!(result.symbols[0].kind, SymbolKind::Struct);
1385 }
1386
1387 #[test]
1388 fn test_parse_async_function() {
1389 let source = "async fn fetch_data() -> Result<(), Error> {}\n";
1390 let result = parse_rust_file(source);
1391 assert!(!result.has_errors);
1392 assert_eq!(result.symbols.len(), 1);
1393 assert_eq!(result.symbols[0].name, "fetch_data");
1394 assert_eq!(result.symbols[0].kind, SymbolKind::Function);
1395 }
1396
1397 #[test]
1398 fn test_type_refs_generic_parameters() {
1399 let source = r#"
1400struct Wrapper<T> { inner: T }
1401fn process(w: Wrapper<Config>) -> Wrapper<Config> { w }
1402"#;
1403 let result = parse_rust_file(source);
1404 let ref_types: Vec<&str> = result
1405 .type_refs
1406 .iter()
1407 .map(|r| r.to_type.as_str())
1408 .collect();
1409 assert!(
1410 ref_types.contains(&"Config"),
1411 "generic param should be extracted: {:?}",
1412 ref_types
1413 );
1414 }
1415
1416 #[test]
1417 fn test_calls_chain() {
1418 let source = r#"
1419fn chain() {
1420 a();
1421 b();
1422 c();
1423}
1424fn a() {}
1425fn b() {}
1426fn c() {}
1427"#;
1428 let result = parse_rust_file(source);
1429 let callee_names: Vec<&str> = result
1430 .calls
1431 .iter()
1432 .map(|c| c.callee_name.as_str())
1433 .collect();
1434 assert!(callee_names.contains(&"a"));
1435 assert!(callee_names.contains(&"b"));
1436 assert!(callee_names.contains(&"c"));
1437 for call in &result.calls {
1439 assert_eq!(call.caller_name, "chain");
1440 }
1441 }
1442
1443 #[test]
1444 fn test_parse_malformed_file() {
1445 let result = parse_rust_file("fn broken( { }");
1446 assert!(result.has_errors);
1447 }
1448
1449 #[test]
1450 fn test_parse_enum_with_variants_methods() {
1451 let source = r#"
1452enum Shape {
1453 Circle(f64),
1454 Rectangle(f64, f64),
1455}
1456
1457impl Shape {
1458 fn area(&self) -> f64 { 0.0 }
1459}
1460"#;
1461 let result = parse_rust_file(source);
1462 assert!(!result.has_errors);
1463 let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
1464 assert!(names.contains(&"Shape"));
1465 assert!(names.contains(&"area"));
1466 }
1467
1468 #[test]
1469 fn test_impl_item_methods_extracted_as_toplevel() {
1470 let source = r#"
1474struct Foo;
1475impl Foo {
1476 fn bar(&self) {}
1477}
1478"#;
1479 let result = parse_rust_file(source);
1480 assert!(!result.has_errors);
1481 let bar = result.symbols.iter().find(|s| s.name == "bar").unwrap();
1482 assert!(bar.kind == SymbolKind::Function || bar.kind == SymbolKind::Method);
1484 }
1485
1486 #[test]
1487 fn test_resolve_self_module() {
1488 let tmp = tempfile::tempdir().unwrap();
1489 let src = tmp.path().join("src").join("db");
1490 fs::create_dir_all(&src).unwrap();
1491 fs::write(src.join("queries.rs"), "").unwrap();
1492
1493 let result = resolve_rust_use("self::queries", "src/db/mod.rs", tmp.path());
1494 assert!(result.is_some());
1495 assert!(result.unwrap().ends_with("queries.rs"));
1496 }
1497
1498 #[test]
1499 fn test_language_parser_trait_name() {
1500 let parser = RustParser;
1501 assert_eq!(parser.language_name(), "rust");
1502 }
1503
1504 #[test]
1505 fn test_language_parser_parse_via_trait() {
1506 let parser = RustParser;
1507 let result = parser.parse("fn foo() {}", &PathBuf::from("test.rs"));
1508 assert!(!result.has_errors);
1509 assert_eq!(result.symbols.len(), 1);
1510 assert_eq!(result.symbols[0].name, "foo");
1511 }
1512}