use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::schema::CodeNode;
pub struct RustModuleResolver {
module_to_file: HashMap<String, PathBuf>,
name_to_node_id: HashMap<String, String>,
crate_name: String,
}
impl RustModuleResolver {
pub fn from_crate(crate_root: &Path) -> Option<Self> {
let src_dir = crate_root.join("src");
if !src_dir.exists() {
return None;
}
let crate_name = detect_crate_name(crate_root);
let mut module_to_file = HashMap::new();
if src_dir.join("lib.rs").exists() {
module_to_file.insert("crate".to_string(), src_dir.join("lib.rs"));
} else if src_dir.join("main.rs").exists() {
module_to_file.insert("crate".to_string(), src_dir.join("main.rs"));
}
walk_modules(&src_dir, "crate", &mut module_to_file);
Some(Self {
module_to_file,
name_to_node_id: HashMap::new(),
crate_name,
})
}
pub fn index_nodes(&mut self, nodes: &[CodeNode]) {
for node in nodes {
if let Some(ref file_path) = node.file_path {
let module_path = self.file_to_module(Path::new(file_path));
if let Some(mod_path) = module_path {
let qualified = format!("{}::{}", mod_path, node.name);
self.name_to_node_id.insert(qualified, node.id.clone());
}
let module_path = self.file_to_module(Path::new(file_path));
if let Some(mod_path) = module_path {
let external = mod_path.replacen("crate", &self.crate_name, 1);
let qualified = format!("{}::{}", external, node.name);
self.name_to_node_id.insert(qualified, node.id.clone());
}
self.name_to_node_id
.entry(node.name.clone())
.or_insert_with(|| node.id.clone());
}
}
}
pub fn resolve_use(&self, module: &str, names: &[String]) -> Vec<(String, String)> {
let mut resolved = Vec::new();
for name in names {
if name == "*" {
continue; }
let qualified = format!("{module}::{name}");
if let Some(node_id) = self.name_to_node_id.get(&qualified) {
resolved.push((name.clone(), node_id.clone()));
continue;
}
let external = qualified.replacen("crate", &self.crate_name, 1);
if let Some(node_id) = self.name_to_node_id.get(&external) {
resolved.push((name.clone(), node_id.clone()));
continue;
}
if let Some(node_id) = self.name_to_node_id.get(name.as_str()) {
resolved.push((name.clone(), node_id.clone()));
}
}
resolved
}
pub fn resolve_name(&self, name: &str) -> Option<String> {
self.name_to_node_id.get(name).cloned()
}
fn file_to_module(&self, file_path: &Path) -> Option<String> {
for (mod_path, path) in &self.module_to_file {
if path.ends_with(file_path) || file_path.ends_with(path) {
return Some(mod_path.clone());
}
}
let path_str = file_path.to_string_lossy();
if let Some(src_idx) = path_str.find("src/") {
let relative = &path_str[src_idx + 4..];
let module = relative
.trim_end_matches(".rs")
.trim_end_matches("/mod")
.replace('/', "::");
if module == "lib" || module == "main" {
return Some("crate".to_string());
}
return Some(format!("crate::{module}"));
}
None
}
pub fn module_count(&self) -> usize {
self.module_to_file.len()
}
pub fn name_count(&self) -> usize {
self.name_to_node_id.len()
}
}
fn walk_modules(dir: &Path, parent_module: &str, map: &mut HashMap<String, PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if path.is_file() && name.ends_with(".rs") && name != "lib.rs" && name != "main.rs" {
let mod_name = name.trim_end_matches(".rs");
let module_path = format!("{parent_module}::{mod_name}");
map.insert(module_path.clone(), path.clone());
let subdir = dir.join(mod_name);
if subdir.is_dir() {
walk_modules(&subdir, &module_path, map);
}
} else if path.is_dir() && path.join("mod.rs").exists() {
let module_path = format!("{parent_module}::{name}");
map.insert(module_path.clone(), path.join("mod.rs"));
walk_modules(&path, &module_path, map);
}
}
}
fn detect_crate_name(crate_root: &Path) -> String {
let cargo_toml = crate_root.join("Cargo.toml");
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
for line in content.lines() {
if let Some(name) = line.strip_prefix("name") {
let name = name.trim().trim_start_matches('=').trim().trim_matches('"');
return name.replace('-', "_");
}
}
}
crate_root
.file_name()
.map(|n| n.to_string_lossy().replace('-', "_"))
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{CodeNode, CodeNodeKind};
#[test]
fn test_from_crate_finds_modules() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
assert!(resolver.module_count() > 0);
}
#[test]
fn test_index_nodes_and_resolve() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
let nodes = vec![CodeNode {
id: "rust_struct:src/schema.rs::CodeNode".to_string(),
kind: CodeNodeKind::RustStruct,
name: "CodeNode".to_string(),
file_path: Some("src/schema.rs".to_string()),
..Default::default()
}];
resolver.index_nodes(&nodes);
assert!(resolver.name_count() > 0);
let resolved = resolver.resolve_name("CodeNode");
assert!(resolved.is_some());
assert!(resolved.unwrap().contains("CodeNode"));
}
#[test]
fn test_resolve_use_crate_path() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
let nodes = vec![CodeNode {
id: "rust_fn:src/ingest.rs::ingest_directory".to_string(),
kind: CodeNodeKind::RustFn,
name: "ingest_directory".to_string(),
file_path: Some("src/ingest.rs".to_string()),
..Default::default()
}];
resolver.index_nodes(&nodes);
let resolved = resolver.resolve_use("crate::ingest", &["ingest_directory".to_string()]);
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].0, "ingest_directory");
}
#[test]
fn test_resolve_external_crate_returns_empty() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
let resolved = resolver.resolve_use("std::collections", &["HashMap".to_string()]);
assert!(resolved.is_empty());
}
#[test]
fn test_detect_crate_name() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let name = detect_crate_name(&crate_root);
assert_eq!(name, "nusy_codegraph");
}
#[test]
fn test_file_to_module() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
let module = resolver.file_to_module(Path::new("src/schema.rs"));
assert_eq!(module, Some("crate::schema".to_string()));
let module = resolver.file_to_module(Path::new("src/lib.rs"));
assert_eq!(module, Some("crate".to_string()));
}
#[test]
fn test_glob_imports_skipped() {
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let resolver = RustModuleResolver::from_crate(&crate_root).expect("should resolve");
let resolved = resolver.resolve_use("crate::schema", &["*".to_string()]);
assert!(resolved.is_empty());
}
}