use std::path::{Path, PathBuf};
use tree_sitter::Language;
use astmap_core::SymbolKind;
use super::{
collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
LanguageResolver, LanguageSupport, ParseResult,
};
pub(super) fn lang() -> LanguageSupport {
LanguageSupport {
name: "rust",
extensions: &["rs"],
parser: &RustParser,
resolver_factory: |_root| Box::new(RustResolver),
config_files: &[],
sibling_fn: rust_sibling_expansion,
}
}
fn rust_sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
let stem = rel_path.strip_suffix(".rs")?;
Some(astmap_core::SiblingExpansion {
prefix: format!("{}/", stem),
extension: String::new(), root_only: false,
})
}
pub(crate) struct RustParser;
impl LanguageParser for RustParser {
fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
parse_rust_file(source)
}
fn language_name(&self) -> &str {
"rust"
}
}
fn rust_language() -> Language {
tree_sitter_rust::LANGUAGE.into()
}
fn parse_rust_file(source: &str) -> ParseResult {
parse_with_tree_sitter(
source,
&rust_language(),
|root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
extract_calls,
|root, src, symbols| {
collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_builtin_type)
},
)
}
fn extract_from_node(
node: tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_index: Option<usize>,
) {
let kind = node.kind();
match kind {
"struct_item" | "enum_item" | "trait_item" | "impl_item" | "type_item" => {
if let Some(sym) = extract_symbol(&node, source, kind, parent_index) {
let idx = symbols.len();
symbols.push(sym);
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, Some(idx));
}
}
return;
}
}
"function_item" => {
if let Some(sym) = extract_function(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"function_signature_item" => {
if let Some(sym) = extract_function(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"mod_item" => {
if let Some(sym) = extract_mod(&node, source, parent_index) {
let idx = symbols.len();
symbols.push(sym);
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, Some(idx));
}
}
return;
}
}
"use_declaration" => {
if let Some(imp) = extract_use(&node, source) {
imports.push(imp);
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, parent_index);
}
}
}
fn extract_symbol(
node: &tree_sitter::Node,
source: &str,
node_kind: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let kind = match node_kind {
"struct_item" => SymbolKind::Struct,
"enum_item" => SymbolKind::Enum,
"trait_item" => SymbolKind::Trait,
"impl_item" => {
let type_node = node.child_by_field_name("type")?;
let type_name = node_text(&type_node, source);
return Some(symbol_from_node(
type_name,
SymbolKind::Impl,
node,
source,
parent_index,
));
}
"type_item" => SymbolKind::TypeAlias,
_ => return None,
};
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_function(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let kind = if parent_index.is_some() {
SymbolKind::Method
} else {
SymbolKind::Function
};
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_mod(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Module,
node,
source,
parent_index,
))
}
fn extract_use(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
let text = node_text(node, source);
let path = text
.strip_prefix("use ")
.unwrap_or(&text)
.trim_end_matches(';')
.trim()
.to_string();
let imported_symbols = extract_imported_names_from_use(&path);
Some(ExtractedImport {
path,
imported_symbols,
})
}
fn extract_imported_names_from_use(path: &str) -> Vec<String> {
if let Some(brace_start) = path.find('{') {
if let Some(brace_end) = path.rfind('}') {
let inner = &path[brace_start + 1..brace_end];
return inner
.split(',')
.map(|s| resolve_use_alias(s.trim()))
.filter(|s| !s.is_empty() && s != "*" && s != "self")
.collect();
}
}
if path.ends_with("::*") {
return vec![];
}
if let Some(last) = path.rsplit("::").next() {
let name = resolve_use_alias(last.trim());
if !name.is_empty() && name != "self" {
return vec![name];
}
}
vec![]
}
fn resolve_use_alias(item: &str) -> String {
if let Some(pos) = item.find(" as ") {
return item[pos + 4..].trim().to_string();
}
if let Some(last) = item.rsplit("::").next() {
return last.trim().to_string();
}
item.to_string()
}
fn extract_calls(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
) -> Vec<ExtractedCall> {
let mut calls = Vec::new();
collect_calls(root, source, symbols, &mut calls);
calls
}
fn collect_calls(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
calls: &mut Vec<ExtractedCall>,
) {
if node.kind() == "call_expression" {
if let Some(func_node) = node.child_by_field_name("function") {
let callee_text = node_text(&func_node, source);
let callee_name = callee_text.rsplit("::").next().unwrap_or(&callee_text);
let callee_name = callee_name.rsplit('.').next().unwrap_or(callee_name);
let call_line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name: callee_name.to_string(),
line: call_line,
});
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_calls(child, source, symbols, calls);
}
}
}
fn is_builtin_type(name: &str) -> bool {
matches!(
name,
"bool"
| "char"
| "str"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
| "String"
| "Vec"
| "Option"
| "Result"
| "Box"
| "Rc"
| "Arc"
| "HashMap"
| "HashSet"
| "BTreeMap"
| "BTreeSet"
| "Path"
| "PathBuf"
| "Self"
)
}
pub(crate) struct RustResolver;
impl LanguageResolver for RustResolver {
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
project_root: &Path,
) -> Option<PathBuf> {
resolve_rust_use(import_path, source_file, project_root)
}
}
pub fn resolve_rust_use(use_path: &str, source_file: &str, project_root: &Path) -> Option<PathBuf> {
let normalized = normalize_use_path(use_path);
let parts: Vec<&str> = normalized.split("::").collect();
if parts.is_empty() {
return None;
}
match parts[0] {
"crate" => {
let crate_src = find_crate_src_dir(source_file, project_root)?;
resolve_module_path(&parts[1..], &crate_src)
}
"super" => {
let source_path = project_root.join(source_file);
let source_dir = source_path.parent()?.to_path_buf();
let file_stem = source_path.file_stem()?.to_string_lossy();
let base = if file_stem == "mod" {
source_dir.parent()?.to_path_buf()
} else {
source_dir
};
resolve_module_path(&parts[1..], &base)
}
"self" => {
let source_dir = project_root.join(source_file).parent()?.to_path_buf();
resolve_module_path(&parts[1..], &source_dir)
}
_ => None,
}
}
fn normalize_use_path(path: &str) -> String {
let mut s = path.to_string();
if let Some(brace_pos) = s.find('{') {
s.truncate(brace_pos);
s = s.trim_end_matches("::").to_string();
}
if s.ends_with("::*") {
s.truncate(s.len() - 3);
}
s
}
fn find_crate_src_dir(source_file: &str, project_root: &Path) -> Option<PathBuf> {
let abs = project_root.join(source_file);
let mut current = abs.parent()?;
while current.starts_with(project_root) {
if current.file_name().is_some_and(|n| n == "src") {
return Some(current.to_path_buf());
}
current = current.parent()?;
}
let fallback = project_root.join("src");
if fallback.exists() {
Some(fallback)
} else {
None
}
}
fn resolve_module_path(parts: &[&str], base_dir: &Path) -> Option<PathBuf> {
if parts.is_empty() {
return None;
}
let mut current = base_dir.to_path_buf();
for (i, part) in parts.iter().enumerate() {
let as_file = current.join(format!("{}.rs", part));
if as_file.exists() {
return Some(as_file);
}
let as_mod = current.join(part).join("mod.rs");
if as_mod.exists() {
return Some(as_mod);
}
if i == parts.len() - 1 {
let current_mod = current.join("mod.rs");
if current_mod.exists() {
return Some(current_mod);
}
if let Some(dir_name) = current.file_name() {
let parent = current.parent()?;
let as_parent_file = parent.join(format!("{}.rs", dir_name.to_string_lossy()));
if as_parent_file.exists() {
return Some(as_parent_file);
}
}
return None;
}
current = current.join(part);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_parse_struct() {
let source = r#"
pub struct MyStruct {
field: i32,
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "MyStruct");
assert_eq!(result.symbols[0].kind, SymbolKind::Struct);
}
#[test]
fn test_parse_function() {
let source = r#"
fn hello(name: &str) -> String {
format!("Hello, {}", name)
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "hello");
assert_eq!(result.symbols[0].kind, SymbolKind::Function);
assert!(result.symbols[0]
.signature
.as_ref()
.unwrap()
.contains("fn hello"));
}
#[test]
fn test_parse_imports() {
let source = r#"
use std::io::Read;
use crate::db::Database;
use super::models::Symbol;
"#;
let result = parse_rust_file(source);
assert_eq!(result.imports.len(), 3);
assert_eq!(result.imports[0].path, "std::io::Read");
assert_eq!(result.imports[1].path, "crate::db::Database");
assert_eq!(result.imports[2].path, "super::models::Symbol");
}
#[test]
fn test_parse_trait_and_impl() {
let source = r#"
pub trait Greet {
fn greet(&self) -> String;
}
pub struct Greeter;
impl Greet for Greeter {
fn greet(&self) -> String {
"hello".to_string()
}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Greet"));
assert!(names.contains(&"Greeter"));
assert!(names.contains(&"greet"));
}
#[test]
fn test_parse_enum() {
let source = r#"
pub enum Color {
Red,
Green,
Blue,
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Color");
assert_eq!(result.symbols[0].kind, SymbolKind::Enum);
}
#[test]
fn test_parse_empty_file() {
let result = parse_rust_file("");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 0);
assert_eq!(result.imports.len(), 0);
}
#[test]
fn test_extract_calls() {
let source = r#"
fn helper() -> i32 { 42 }
fn main() {
let x = helper();
println!("{}", x);
}
"#;
let result = parse_rust_file(source);
assert!(!result.calls.is_empty());
let call_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(call_names.contains(&"helper"), "calls: {:?}", result.calls);
}
#[test]
fn test_extract_type_refs() {
let source = r#"
struct Config {
name: String,
}
struct Database {
config: Config,
}
fn create_db(config: Config) -> Database {
Database { config }
}
"#;
let result = parse_rust_file(source);
let ref_types: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
ref_types.contains(&"Config"),
"type_refs: {:?}",
result.type_refs
);
assert!(
ref_types.contains(&"Database"),
"type_refs: {:?}",
result.type_refs
);
}
#[test]
fn test_method_calls() {
let source = r#"
struct Foo;
impl Foo {
fn bar(&self) -> i32 { 42 }
fn baz(&self) {
let x = self.bar();
}
}
"#;
let result = parse_rust_file(source);
let call_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(call_names.contains(&"bar"), "calls: {:?}", result.calls);
}
#[test]
fn test_parse_type_alias() {
let source = "type MyResult = Result<(), Box<dyn std::error::Error>>;";
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "MyResult");
assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
}
#[test]
fn test_parse_module() {
let source = r#"
mod inner {
fn private_fn() {}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"inner"));
assert!(names.contains(&"private_fn"));
let private_fn = result
.symbols
.iter()
.find(|s| s.name == "private_fn")
.unwrap();
assert!(private_fn.parent_index.is_some());
}
#[test]
fn test_parse_impl_methods_extracted() {
let source = r#"
struct MyStruct;
impl MyStruct {
fn new() -> Self { MyStruct }
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let struct_sym = result
.symbols
.iter()
.find(|s| s.name == "MyStruct")
.unwrap();
assert_eq!(struct_sym.kind, SymbolKind::Struct);
let new_sym = result.symbols.iter().find(|s| s.name == "new").unwrap();
assert!(new_sym.kind == SymbolKind::Function || new_sym.kind == SymbolKind::Method);
}
#[test]
fn test_is_builtin_type_positive() {
assert!(is_builtin_type("bool"));
assert!(is_builtin_type("String"));
assert!(is_builtin_type("Vec"));
assert!(is_builtin_type("Option"));
assert!(is_builtin_type("Result"));
assert!(is_builtin_type("HashMap"));
assert!(is_builtin_type("Self"));
assert!(is_builtin_type("PathBuf"));
}
#[test]
fn test_is_builtin_type_negative() {
assert!(!is_builtin_type("MyStruct"));
assert!(!is_builtin_type("Database"));
assert!(!is_builtin_type("Scanner"));
}
#[test]
fn test_parse_multiple_use_declarations() {
let source = r#"
use std::collections::HashMap;
use crate::db::{Database, DbError};
use super::models::*;
"#;
let result = parse_rust_file(source);
assert_eq!(result.imports.len(), 3);
}
#[test]
fn test_parse_trait_method_signature() {
let source = r#"
pub trait Repository {
fn open(&self) -> Result<(), Error>;
fn close(&self);
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let methods: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.map(|s| s.name.as_str())
.collect();
assert!(methods.contains(&"open"));
assert!(methods.contains(&"close"));
}
#[test]
fn test_path_qualified_call() {
let source = r#"
fn caller() {
std::io::stdout();
Vec::new();
}
"#;
let result = parse_rust_file(source);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"stdout") || callee_names.contains(&"new"));
}
#[test]
fn test_extract_imported_names_simple() {
let names = extract_imported_names_from_use("crate::db::Repository");
assert_eq!(names, vec!["Repository"]);
}
#[test]
fn test_extract_imported_names_group() {
let names = extract_imported_names_from_use("crate::db::{Database, DbError}");
assert_eq!(names, vec!["Database", "DbError"]);
}
#[test]
fn test_extract_imported_names_wildcard() {
let names = extract_imported_names_from_use("super::models::*");
assert!(names.is_empty());
}
#[test]
fn test_extract_imported_names_as_alias() {
let names = extract_imported_names_from_use("crate::db::Repository as Repo");
assert_eq!(names, vec!["Repo"]);
}
#[test]
fn test_extract_imported_names_group_with_alias() {
let names = extract_imported_names_from_use("crate::db::{Repository as Repo, DbError}");
assert_eq!(names, vec!["Repo", "DbError"]);
}
#[test]
fn test_extract_imported_names_self_filtered() {
let names = extract_imported_names_from_use("crate::db::{self, Database}");
assert_eq!(names, vec!["Database"]);
}
#[test]
fn test_extract_imported_names_nested_path() {
let names = extract_imported_names_from_use("std::collections::{HashMap, hash_map::Entry}");
assert_eq!(names, vec!["HashMap", "Entry"]);
}
#[test]
fn test_import_extracts_imported_symbols() {
let source = r#"
use crate::db::{Database, DbError};
use super::models::*;
use crate::scanner::Scanner as Sc;
"#;
let result = parse_rust_file(source);
assert_eq!(result.imports.len(), 3);
assert_eq!(
result.imports[0].imported_symbols,
vec!["Database", "DbError"]
);
assert!(result.imports[1].imported_symbols.is_empty()); assert_eq!(result.imports[2].imported_symbols, vec!["Sc"]);
}
#[test]
fn test_resolve_crate_path_simple() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("mod.rs"), "").unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = resolve_rust_use("crate::db", "src/lib.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("db/mod.rs"));
}
#[test]
fn test_resolve_crate_path_workspace_subcrate() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("my-core").join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("mod.rs"), "").unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = resolve_rust_use("crate::db", "my-core/src/lib.rs", tmp.path());
assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("db/mod.rs"), "got: {}", path.display());
}
#[test]
fn test_resolve_crate_nested_module() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("impact.rs"), "").unwrap();
let result = resolve_rust_use("crate::impact", "src/db/mod.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("impact.rs"));
}
#[test]
fn test_resolve_super() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("mod.rs"), "").unwrap();
fs::write(src.join("db").join("models.rs"), "").unwrap();
let result = resolve_rust_use("super::models", "src/db/queries.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("models.rs"));
}
#[test]
fn test_external_crate_returns_none() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
resolve_rust_use("std::io::Read", "src/lib.rs", tmp.path()),
None
);
assert_eq!(
resolve_rust_use("serde::Deserialize", "src/lib.rs", tmp.path()),
None
);
assert_eq!(
resolve_rust_use("tokio::spawn", "src/lib.rs", tmp.path()),
None
);
}
#[test]
fn test_normalize_use_path_braces() {
assert_eq!(
normalize_use_path("crate::db::{Database, DbError}"),
"crate::db"
);
}
#[test]
fn test_normalize_use_path_wildcard() {
assert_eq!(
normalize_use_path("crate::db::models::*"),
"crate::db::models"
);
}
#[test]
fn test_normalize_use_path_plain() {
assert_eq!(
normalize_use_path("crate::db::Database"),
"crate::db::Database"
);
}
#[test]
fn test_resolve_self() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src").join("db");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("models.rs"), "").unwrap();
let result = resolve_rust_use("self::models", "src/db/mod.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("models.rs"));
}
#[test]
fn test_resolve_crate_symbol_name() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("mod.rs"), "").unwrap();
let result = resolve_rust_use("crate::db::Database", "src/lib.rs", tmp.path());
assert!(result.is_some());
assert!(
result.unwrap().to_string_lossy().contains("db"),
"should resolve to db module"
);
}
#[test]
fn test_resolve_empty_path() {
let tmp = tempfile::tempdir().unwrap();
let result = resolve_rust_use("", "src/lib.rs", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_find_crate_src_dir_direct() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = find_crate_src_dir("src/lib.rs", tmp.path());
assert!(result.is_some());
}
#[test]
fn test_find_crate_src_dir_fallback() {
let tmp = tempfile::tempdir().unwrap();
let config_dir = tmp.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("settings.rs"), "").unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
let result = find_crate_src_dir("config/settings.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("src"));
}
#[test]
fn test_find_crate_src_dir_no_src() {
let tmp = tempfile::tempdir().unwrap();
let config_dir = tmp.path().join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("settings.rs"), "").unwrap();
let result = find_crate_src_dir("config/settings.rs", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_resolve_crate_only() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = resolve_rust_use("crate", "src/lib.rs", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_resolve_module_path_as_rs_file() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("utils.rs"), "").unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = resolve_rust_use("crate::utils", "src/lib.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("utils.rs"));
}
#[test]
fn test_resolve_deep_symbol_to_parent_rs() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("utils.rs"), "").unwrap();
fs::write(src.join("lib.rs"), "").unwrap();
let result = resolve_rust_use("crate::utils::Helper", "src/lib.rs", tmp.path());
assert!(result.is_some());
}
#[test]
fn test_resolve_super_from_mod_rs() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("mod.rs"), "").unwrap();
fs::write(src.join("export.rs"), "").unwrap();
let result = resolve_rust_use("super::export", "src/db/mod.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("export.rs"));
}
#[test]
fn test_resolve_super_from_new_style_module() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db.rs"), "").unwrap();
fs::write(src.join("export.rs"), "").unwrap();
let result = resolve_rust_use("super::export", "src/db.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("export.rs"));
}
#[test]
fn test_resolve_super_nonexistent() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(src.join("db")).unwrap();
fs::write(src.join("db").join("queries.rs"), "").unwrap();
let result = resolve_rust_use("super::nonexistent", "src/db/queries.rs", tmp.path());
assert!(result.is_none());
}
#[test]
fn test_parse_trait_method_signatures_without_body() {
let source = r#"
pub trait Serializer {
fn serialize(&self) -> Vec<u8>;
fn deserialize(data: &[u8]) -> Self;
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let methods: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.map(|s| s.name.as_str())
.collect();
assert!(methods.contains(&"serialize"), "methods: {:?}", methods);
assert!(methods.contains(&"deserialize"), "methods: {:?}", methods);
let trait_idx = result
.symbols
.iter()
.position(|s| s.name == "Serializer")
.unwrap();
for sym in result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
{
assert_eq!(
sym.parent_index,
Some(trait_idx),
"method '{}' should be child of Serializer",
sym.name
);
}
}
#[test]
fn test_parse_nested_module_with_items() {
let source = r#"
mod outer {
mod inner {
fn deep_fn() {}
}
fn outer_fn() {}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"outer"), "symbols: {:?}", names);
assert!(names.contains(&"inner"), "symbols: {:?}", names);
assert!(names.contains(&"deep_fn"), "symbols: {:?}", names);
assert!(names.contains(&"outer_fn"), "symbols: {:?}", names);
let outer_idx = result
.symbols
.iter()
.position(|s| s.name == "outer")
.unwrap();
let inner = result.symbols.iter().find(|s| s.name == "inner").unwrap();
assert_eq!(inner.parent_index, Some(outer_idx));
}
#[test]
fn test_parse_impl_for_trait_methods_extracted() {
let source = r#"
trait Render {
fn render(&self) -> String;
fn refresh(&self);
}
struct Widget;
impl Render for Widget {
fn render(&self) -> String { "widget".to_string() }
fn refresh(&self) {}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let method_names: Vec<&str> = result
.symbols
.iter()
.filter(|s| s.name == "render" || s.name == "refresh")
.map(|s| s.name.as_str())
.collect();
assert!(
method_names.iter().filter(|&&n| n == "render").count() >= 2,
"render should appear in both trait and impl: {:?}",
method_names
);
assert!(
method_names.iter().filter(|&&n| n == "refresh").count() >= 2,
"refresh should appear in both trait and impl: {:?}",
method_names
);
}
#[test]
fn test_resolve_use_alias_plain() {
assert_eq!(resolve_use_alias("Foo"), "Foo");
}
#[test]
fn test_resolve_use_alias_with_as() {
assert_eq!(resolve_use_alias("Foo as Bar"), "Bar");
}
#[test]
fn test_resolve_use_alias_nested() {
assert_eq!(resolve_use_alias("hash_map::Entry"), "Entry");
}
#[test]
fn test_normalize_use_path_combined_braces_and_suffix() {
assert_eq!(normalize_use_path("crate::foo::{A, B}"), "crate::foo");
}
#[test]
fn test_extract_imported_names_empty_path() {
let names = extract_imported_names_from_use("");
assert!(names.is_empty());
}
#[test]
fn test_parse_const_and_static() {
let source = r#"
const MAX: usize = 100;
static COUNTER: i32 = 0;
fn used_fn() {}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"used_fn"));
}
#[test]
fn test_parse_struct_with_impl_methods() {
let source = r#"
pub struct Config {
pub name: String,
}
impl Config {
pub fn new(name: &str) -> Self {
Config { name: name.to_string() }
}
pub fn validate(&self) -> bool {
!self.name.is_empty()
}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let all_names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
all_names.contains(&"Config"),
"struct Config should be extracted"
);
assert!(
all_names.contains(&"new"),
"new should be extracted: {:?}",
all_names
);
assert!(
all_names.contains(&"validate"),
"validate should be extracted: {:?}",
all_names
);
}
#[test]
fn test_resolve_module_path_intermediate_not_dir() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("deep"), "").unwrap();
let result = resolve_rust_use("crate::deep::nested", "src/lib.rs", tmp.path());
assert!(result.is_none(), "intermediate non-directory should fail");
}
#[test]
fn test_parse_use_with_self() {
let source = "use crate::db::{self, models};\n";
let result = parse_rust_file(source);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["models"]);
}
#[test]
fn test_type_ref_builtin_types_filtered() {
let source = r#"
fn foo(a: bool, b: i32, c: u64, d: f32, e: String, f: Vec<i32>) {}
"#;
let result = parse_rust_file(source);
assert!(
result.type_refs.is_empty(),
"builtin types should be filtered: {:?}",
result.type_refs
);
}
#[test]
fn test_call_inside_nested_module() {
let source = r#"
mod setup {
fn init() {
helper();
}
fn helper() {}
}
"#;
let result = parse_rust_file(source);
for call in &result.calls {
assert!(
!call.caller_name.is_empty(),
"call to '{}' should have a caller",
call.callee_name
);
}
}
#[test]
fn test_extract_imported_names_single_no_path_separator() {
let names = extract_imported_names_from_use("Foo");
assert_eq!(names, vec!["Foo"]);
}
#[test]
fn test_extract_imported_names_self_only() {
let names = extract_imported_names_from_use("crate::db::self");
assert!(
names.is_empty(),
"self-only import should return empty: {:?}",
names
);
}
#[test]
fn test_type_refs_from_struct_fields() {
let source = r#"
struct Config { name: String }
struct App { config: Config, count: i32 }
"#;
let result = parse_rust_file(source);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
type_names.contains(&"Config"),
"Config type ref should be found: {:?}",
type_names
);
}
#[test]
fn test_resolve_module_path_empty_parts() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
let result = resolve_module_path(&[], &src);
assert!(result.is_none(), "empty parts should return None");
}
#[test]
fn test_parse_use_with_wildcard_in_group() {
let source = "use crate::db::{*, DbError};\n";
let result = parse_rust_file(source);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].imported_symbols, vec!["DbError"]);
}
#[test]
fn test_is_builtin_type_all_values() {
for ty in &[
"bool", "char", "str", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32",
"u64", "u128", "usize", "f32", "f64", "String", "Vec", "Option", "Result", "Box", "Rc",
"Arc", "HashMap", "HashSet", "BTreeMap", "BTreeSet", "Path", "PathBuf", "Self",
] {
assert!(is_builtin_type(ty), "'{}' should be builtin", ty);
}
}
#[test]
fn test_normalize_use_path_empty() {
assert_eq!(normalize_use_path(""), "");
}
#[test]
fn test_normalize_use_path_single_ident() {
assert_eq!(normalize_use_path("std"), "std");
}
#[test]
fn test_parse_multiple_impl_blocks() {
let source = r#"
struct Widget;
impl Widget {
fn new() -> Self { Widget }
fn render(&self) {}
}
impl Widget {
fn destroy(&self) {}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Widget"));
assert!(names.contains(&"new"));
assert!(names.contains(&"render"));
assert!(names.contains(&"destroy"));
}
#[test]
fn test_parse_generic_struct() {
let source = r#"
pub struct Container<T: Clone> {
inner: Vec<T>,
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Container");
assert_eq!(result.symbols[0].kind, SymbolKind::Struct);
}
#[test]
fn test_parse_async_function() {
let source = "async fn fetch_data() -> Result<(), Error> {}\n";
let result = parse_rust_file(source);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "fetch_data");
assert_eq!(result.symbols[0].kind, SymbolKind::Function);
}
#[test]
fn test_type_refs_generic_parameters() {
let source = r#"
struct Wrapper<T> { inner: T }
fn process(w: Wrapper<Config>) -> Wrapper<Config> { w }
"#;
let result = parse_rust_file(source);
let ref_types: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
ref_types.contains(&"Config"),
"generic param should be extracted: {:?}",
ref_types
);
}
#[test]
fn test_calls_chain() {
let source = r#"
fn chain() {
a();
b();
c();
}
fn a() {}
fn b() {}
fn c() {}
"#;
let result = parse_rust_file(source);
let callee_names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callee_names.contains(&"a"));
assert!(callee_names.contains(&"b"));
assert!(callee_names.contains(&"c"));
for call in &result.calls {
assert_eq!(call.caller_name, "chain");
}
}
#[test]
fn test_parse_malformed_file() {
let result = parse_rust_file("fn broken( { }");
assert!(result.has_errors);
}
#[test]
fn test_parse_enum_with_variants_methods() {
let source = r#"
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
impl Shape {
fn area(&self) -> f64 { 0.0 }
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Shape"));
assert!(names.contains(&"area"));
}
#[test]
fn test_impl_item_methods_extracted_as_toplevel() {
let source = r#"
struct Foo;
impl Foo {
fn bar(&self) {}
}
"#;
let result = parse_rust_file(source);
assert!(!result.has_errors);
let bar = result.symbols.iter().find(|s| s.name == "bar").unwrap();
assert!(bar.kind == SymbolKind::Function || bar.kind == SymbolKind::Method);
}
#[test]
fn test_resolve_self_module() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src").join("db");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("queries.rs"), "").unwrap();
let result = resolve_rust_use("self::queries", "src/db/mod.rs", tmp.path());
assert!(result.is_some());
assert!(result.unwrap().ends_with("queries.rs"));
}
#[test]
fn test_language_parser_trait_name() {
let parser = RustParser;
assert_eq!(parser.language_name(), "rust");
}
#[test]
fn test_language_parser_parse_via_trait() {
let parser = RustParser;
let result = parser.parse("fn foo() {}", &PathBuf::from("test.rs"));
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "foo");
}
}