Skip to main content

astmap_languages/language/
rust.rs

1// Rust language support: parser + resolver
2
3use 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(), // match any file under the submodule dir
31        root_only: false,
32    })
33}
34
35// ── Parser ──
36
37pub(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
204/// Extract the specific symbol names from a Rust use path.
205/// e.g. "crate::db::Repository" → ["Repository"]
206///      "crate::db::{Database, DbError}" → ["Database", "DbError"]
207///      "super::models::*" → []
208fn extract_imported_names_from_use(path: &str) -> Vec<String> {
209    // Handle group imports: crate::db::{Foo, Bar}
210    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    // Handle wildcard
221    if path.ends_with("::*") {
222        return vec![];
223    }
224    // Simple import: take the last segment, handle "as" alias
225    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
234/// Resolve a Rust use item that may contain "as" alias or a nested path.
235/// "Foo as Bar" → "Bar" (the local alias)
236/// "hash_map::Entry" → "Entry" (last segment of nested path)
237/// "Foo" → "Foo"
238fn resolve_use_alias(item: &str) -> String {
239    // Handle "as" alias: "Foo as Bar" → "Bar"
240    if let Some(pos) = item.find(" as ") {
241        return item[pos + 4..].trim().to_string();
242    }
243    // Handle nested path: "hash_map::Entry" → "Entry"
244    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
328// ── Resolver ──
329
330pub(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
343/// Resolve a Rust use path to a file path relative to the project root.
344///
345/// `source_file` is relative to project_root (e.g., "astmap-core/src/db/mod.rs").
346/// Returns an absolute path, or None for external crate imports.
347pub 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    // ── Parser tests ──
449
450    #[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()); // wildcard
792        assert_eq!(result.imports[2].imported_symbols, vec!["Sc"]);
793    }
794
795    // ── Resolver tests ──
796
797    #[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    // --- Additional coverage tests ---
1034
1035    #[test]
1036    fn test_parse_trait_method_signatures_without_body() {
1037        // function_signature_item branch (trait methods without default impl)
1038        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        // They should be children of the trait
1055        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        // inner should be child of outer
1092        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        // impl with trait clause: methods inside should still be extracted
1104        // even though the impl block itself may not have a "name" field
1105        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        // Each name appears twice: once in trait (signature_item) and once in impl
1125        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        // No "as" and no "::" => return as-is
1140        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        // Path with braces should strip everything from brace onwards
1156        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        // These should not be extracted (not in match arms)
1168        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        // Only the function should be extracted
1176        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        // impl Config extracts as impl kind with "Config" name; methods are its children
1199        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        // When an intermediate part is not a directory, resolution should fail
1219        let tmp = tempfile::tempdir().unwrap();
1220        let src = tmp.path().join("src");
1221        fs::create_dir_all(&src).unwrap();
1222        // "deep" is a file, not a directory
1223        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        // "self" should be filtered, only "models" kept
1235        assert_eq!(result.imports[0].imported_symbols, vec!["models"]);
1236    }
1237
1238    #[test]
1239    fn test_type_ref_builtin_types_filtered() {
1240        // All builtin types should NOT appear in type_refs
1241        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        // All calls should have a caller
1264        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        // "*" should be filtered, only "DbError" kept
1323        assert_eq!(result.imports[0].imported_symbols, vec!["DbError"]);
1324    }
1325
1326    #[test]
1327    fn test_is_builtin_type_all_values() {
1328        // Exhaustively test all builtin types
1329        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    // ── Additional edge case tests ──
1339
1340    #[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        // All calls should have "chain" as caller
1438        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        // impl blocks don't have a "name" field, so extract_symbol returns None.
1471        // Methods inside are extracted but fall through to the default recursion,
1472        // meaning they are extracted without a parent_index.
1473        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        // bar is a function (no parent since impl block is not extracted as a symbol)
1483        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}