use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use tree_sitter::Tree;
use crate::parser::PARSER_RS;
#[derive(Debug, Default)]
pub struct RustModTree {
pub mod_map: HashMap<String, PathBuf>,
pub reverse_map: HashMap<PathBuf, String>,
}
impl RustModTree {
pub fn resolve_module_path(&self, path: &str) -> Option<&PathBuf> {
if let Some(file) = self.mod_map.get(path) {
return Some(file);
}
let mut current = path;
loop {
match current.rfind("::") {
None => return None,
Some(idx) => {
current = ¤t[..idx];
if let Some(file) = self.mod_map.get(current) {
return Some(file);
}
}
}
}
}
pub fn file_to_module_path(&self, file: &Path) -> Option<&String> {
if let Some(mp) = self.reverse_map.get(file) {
return Some(mp);
}
if let Ok(canonical) = file.canonicalize() {
return self.reverse_map.get(&canonical);
}
None
}
}
pub fn find_crate_root(cargo_toml_path: &Path) -> Option<(String, PathBuf)> {
let content = std::fs::read_to_string(cargo_toml_path).ok()?;
let manifest: toml::Value = toml::from_str(&content).ok()?;
let raw_name = manifest.get("package")?.get("name")?.as_str()?;
let crate_name = raw_name.replace('-', "_");
let crate_dir = cargo_toml_path.parent()?;
if let Some(lib_path) = manifest
.get("lib")
.and_then(|l| l.get("path"))
.and_then(|p| p.as_str())
{
let path = crate_dir.join(lib_path);
if path.exists() {
return Some((crate_name, path));
}
}
let lib_rs = crate_dir.join("src").join("lib.rs");
if lib_rs.exists() {
return Some((crate_name, lib_rs));
}
let main_rs = crate_dir.join("src").join("main.rs");
if main_rs.exists() {
return Some((crate_name, main_rs));
}
None
}
pub fn extract_mod_declarations(tree: &Tree, source: &[u8]) -> Vec<String> {
let mut mods = Vec::new();
let root = tree.root_node();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() != "mod_item" {
continue;
}
if child.child_by_field_name("body").is_some() {
continue; }
if let Some(name_node) = child.child_by_field_name("name") {
let name = name_node.utf8_text(source).unwrap_or("").to_owned();
if !name.is_empty() {
mods.push(name);
}
}
}
mods
}
pub fn walk_mod_tree(
current_path: &str,
file: &Path,
mod_map: &mut HashMap<String, PathBuf>,
reverse_map: &mut HashMap<PathBuf, String>,
visited: &mut HashSet<PathBuf>,
) {
let canonical = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
if !visited.insert(canonical.clone()) {
return; }
mod_map.insert(current_path.to_string(), file.to_path_buf());
reverse_map.insert(file.to_path_buf(), current_path.to_string());
if canonical != file.to_path_buf() {
reverse_map.insert(canonical, current_path.to_string());
}
let source = match std::fs::read(file) {
Ok(bytes) => bytes,
Err(_) => return, };
let tree = PARSER_RS.with(|p| p.borrow_mut().parse(&source, None));
let tree = match tree {
Some(t) => t,
None => return, };
let mod_names = extract_mod_declarations(&tree, &source);
let parent_dir = match file.parent() {
Some(d) => d,
None => return,
};
let file_name = file.file_name().and_then(|n| n.to_str()).unwrap_or("");
let is_directory_owner = matches!(file_name, "mod.rs" | "lib.rs" | "main.rs");
let sub_dir = if is_directory_owner {
parent_dir.to_path_buf()
} else {
let stem = file.file_stem().and_then(|s| s.to_str()).unwrap_or("");
parent_dir.join(stem)
};
for mod_name in mod_names {
let candidate_file = sub_dir.join(format!("{mod_name}.rs"));
let candidate_dir = sub_dir.join(&mod_name).join("mod.rs");
let child_file = if candidate_file.exists() {
candidate_file
} else if candidate_dir.exists() {
candidate_dir
} else {
continue;
};
let child_path = format!("{current_path}::{mod_name}");
walk_mod_tree(&child_path, &child_file, mod_map, reverse_map, visited);
}
}
pub fn build_mod_tree(crate_name: &str, crate_root: &Path) -> RustModTree {
let _ = crate_name; let mut mod_map = HashMap::new();
let mut reverse_map = HashMap::new();
let mut visited = HashSet::new();
walk_mod_tree(
"crate",
crate_root,
&mut mod_map,
&mut reverse_map,
&mut visited,
);
RustModTree {
mod_map,
reverse_map,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_simple_crate(root: &Path) {
fs::create_dir_all(root.join("src")).unwrap();
fs::write(root.join("src/lib.rs"), "pub mod parser;\npub mod utils;\n").unwrap();
fs::write(root.join("src/parser.rs"), "pub mod imports;\n").unwrap();
fs::create_dir_all(root.join("src/parser")).unwrap();
fs::write(root.join("src/parser/imports.rs"), "// imports module\n").unwrap();
fs::write(root.join("src/utils.rs"), "// utils module\n").unwrap();
fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
)
.unwrap();
}
#[test]
fn test_find_crate_root_lib() {
let tmp = tempfile::tempdir().unwrap();
make_simple_crate(tmp.path());
let (name, root) = find_crate_root(&tmp.path().join("Cargo.toml")).unwrap();
assert_eq!(name, "my_crate");
assert!(root.ends_with("src/lib.rs"));
}
#[test]
fn test_find_crate_root_main() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path();
std::fs::create_dir_all(p.join("src")).unwrap();
std::fs::write(p.join("src/main.rs"), "fn main() {}").unwrap();
std::fs::write(
p.join("Cargo.toml"),
"[package]\nname = \"my-bin\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let (name, root) = find_crate_root(&p.join("Cargo.toml")).unwrap();
assert_eq!(name, "my_bin");
assert!(root.ends_with("src/main.rs"));
}
#[test]
fn test_build_mod_tree_maps_all_modules() {
let tmp = tempfile::tempdir().unwrap();
make_simple_crate(tmp.path());
let crate_root = tmp.path().join("src/lib.rs");
let tree = build_mod_tree("my_crate", &crate_root);
assert!(
tree.mod_map.contains_key("crate"),
"crate root must be in map"
);
assert!(
tree.mod_map.contains_key("crate::parser"),
"crate::parser must be in map"
);
assert!(
tree.mod_map.contains_key("crate::utils"),
"crate::utils must be in map"
);
assert!(
tree.mod_map.contains_key("crate::parser::imports"),
"crate::parser::imports must be in map"
);
}
#[test]
fn test_resolve_module_path_strips_symbol_segment() {
let tmp = tempfile::tempdir().unwrap();
make_simple_crate(tmp.path());
let crate_root = tmp.path().join("src/lib.rs");
let tree = build_mod_tree("my_crate", &crate_root);
let result = tree.resolve_module_path("crate::parser::imports::SomeSymbol");
assert!(
result.is_some(),
"should resolve by stripping symbol segment"
);
assert!(
result.unwrap().ends_with("imports.rs"),
"should resolve to imports.rs"
);
}
#[test]
fn test_reverse_map_populated() {
let tmp = tempfile::tempdir().unwrap();
make_simple_crate(tmp.path());
let crate_root = tmp.path().join("src/lib.rs");
let tree = build_mod_tree("my_crate", &crate_root);
let utils_path = tmp.path().join("src/utils.rs");
let mod_path = tree.file_to_module_path(&utils_path);
assert_eq!(mod_path.map(|s| s.as_str()), Some("crate::utils"));
}
#[test]
fn test_inline_mod_skipped() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path();
std::fs::create_dir_all(p.join("src")).unwrap();
std::fs::write(
p.join("src/lib.rs"),
"mod inline { pub fn foo() {} }\npub mod file_backed;\n",
)
.unwrap();
std::fs::write(p.join("src/file_backed.rs"), "// file-backed module\n").unwrap();
std::fs::write(
p.join("Cargo.toml"),
"[package]\nname = \"test-crate\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let tree = build_mod_tree("test_crate", &p.join("src/lib.rs"));
assert!(
!tree.mod_map.contains_key("crate::inline"),
"inline mod must not be in mod_map"
);
assert!(
tree.mod_map.contains_key("crate::file_backed"),
"file-backed mod must be in mod_map"
);
}
}