use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn read_module_file_within_root(allowed_root: &Path, file_path: &Path) -> std::io::Result<String> {
crate::fs_utils::read_to_string_within(allowed_root, file_path)
}
pub(super) fn parse_rust_brace_names(raw: &str) -> Vec<(String, String)> {
raw.split(',')
.filter_map(|item| {
let trimmed = item.trim();
if trimmed.is_empty() {
return None;
}
if trimmed == "self" {
return None;
}
if let Some((original, alias)) = trimmed.split_once(" as ") {
let original_name = original
.trim()
.rsplit("::")
.next()
.unwrap_or(original.trim());
Some((original_name.to_string(), alias.trim().to_string()))
} else {
let last_segment = trimmed.rsplit("::").next().unwrap_or(trimmed).trim();
if last_segment.is_empty() {
None
} else {
Some((last_segment.to_string(), last_segment.to_string()))
}
}
})
.collect()
}
#[derive(Debug, Clone)]
pub struct CrateModuleMap {
modules: HashMap<String, PathBuf>,
crate_root: PathBuf,
}
impl CrateModuleMap {
pub fn build(crate_root: &Path) -> std::io::Result<Self> {
let mut map = CrateModuleMap {
modules: HashMap::new(),
crate_root: crate_root.to_path_buf(),
};
let lib_rs = crate_root.join("src").join("lib.rs");
let main_rs = crate_root.join("src").join("main.rs");
let entry_point = if lib_rs.exists() {
lib_rs
} else if main_rs.exists() {
main_rs
} else {
return Ok(map); };
map.scan_module(&entry_point, "")?;
Ok(map)
}
fn scan_module(&mut self, file_path: &Path, module_prefix: &str) -> std::io::Result<()> {
let content = read_module_file_within_root(&self.crate_root, file_path)?;
let mod_regex = regex::Regex::new(r"(?m)^\s*(?:pub\s+)?mod\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*;")
.expect("valid mod regex");
for caps in mod_regex.captures_iter(&content) {
if let Some(mod_name) = caps.get(1) {
let mod_name = mod_name.as_str();
let module_path = if module_prefix.is_empty() {
mod_name.to_string()
} else {
format!("{}::{}", module_prefix, mod_name)
};
let parent = file_path.parent().unwrap_or(file_path);
let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let search_dirs: Vec<PathBuf> = if file_name.ends_with(".rs")
&& file_name != "mod.rs"
&& file_name != "lib.rs"
&& file_name != "main.rs"
{
let module_dir = parent.join(file_name.strip_suffix(".rs").unwrap());
vec![module_dir, parent.to_path_buf()]
} else {
vec![parent.to_path_buf()]
};
let mut found = false;
for search_dir in search_dirs {
let mod_file = search_dir.join(format!("{}.rs", mod_name));
let mod_dir_file = search_dir.join(mod_name).join("mod.rs");
if mod_file.exists() {
if let Ok(relative) = mod_file.strip_prefix(&self.crate_root) {
self.modules
.insert(module_path.clone(), relative.to_path_buf());
}
let _ = self.scan_module(&mod_file, &module_path);
found = true;
break;
} else if mod_dir_file.exists() {
if let Ok(relative) = mod_dir_file.strip_prefix(&self.crate_root) {
self.modules
.insert(module_path.clone(), relative.to_path_buf());
}
let _ = self.scan_module(&mod_dir_file, &module_path);
found = true;
break;
}
}
if !found {
}
}
}
Ok(())
}
pub fn resolve_module_path(&self, from_file: &Path, import_path: &str) -> Option<PathBuf> {
if let Some(rest) = import_path.strip_prefix("crate::") {
return self.resolve_absolute(rest);
}
let current_module = self.file_to_module_path(from_file)?;
if let Some(rest) = import_path.strip_prefix("super::") {
let parent_module = self.parent_module(¤t_module)?;
let target_path = if rest.is_empty() {
parent_module
} else {
format!("{}::{}", parent_module, rest)
};
return self.resolve_absolute(&target_path);
}
if let Some(rest) = import_path.strip_prefix("self::") {
let target_path = format!("{}::{}", current_module, rest);
return self.resolve_absolute(&target_path);
}
let mut paths_to_try = Vec::new();
if !current_module.is_empty() {
paths_to_try.push(format!("{}::{}", current_module, import_path));
let mut current = current_module.to_string();
while !current.is_empty() {
if let Some(parent) = self.parent_module(¤t) {
if parent.is_empty() {
paths_to_try.push(import_path.to_string());
} else {
paths_to_try.push(format!("{}::{}", parent, import_path));
}
current = parent;
} else {
paths_to_try.push(import_path.to_string());
break;
}
}
} else {
paths_to_try.push(import_path.to_string());
}
for path in paths_to_try {
if let Some(resolved) = self.resolve_absolute_exact(&path) {
return Some(resolved);
}
}
self.resolve_absolute(import_path)
}
fn resolve_absolute_exact(&self, module_path: &str) -> Option<PathBuf> {
self.modules.get(module_path).cloned()
}
fn resolve_absolute(&self, module_path: &str) -> Option<PathBuf> {
self.modules.get(module_path).cloned().or_else(|| {
let mut parts: Vec<&str> = module_path.split("::").collect();
while !parts.is_empty() {
parts.pop();
let partial = parts.join("::");
if let Some(path) = self.modules.get(&partial) {
return Some(path.clone());
}
}
None
})
}
fn file_to_module_path(&self, file_path: &Path) -> Option<String> {
let relative = file_path.strip_prefix(&self.crate_root).ok()?;
let mut parts = Vec::new();
let path_components: Vec<_> = relative.components().collect();
for (i, component) in path_components.iter().enumerate() {
let component_str = component.as_os_str().to_str()?;
if component_str == "src" {
continue;
}
let is_last = i == path_components.len() - 1;
if is_last {
if component_str == "lib.rs" || component_str == "main.rs" {
break; }
if component_str == "mod.rs" {
break;
}
if component_str.ends_with(".rs") {
let name = component_str.strip_suffix(".rs").unwrap_or(component_str);
parts.push(name);
}
} else {
parts.push(component_str);
}
}
if parts.is_empty() {
Some(String::new()) } else {
Some(parts.join("::"))
}
}
fn parent_module(&self, module_path: &str) -> Option<String> {
if module_path.is_empty() {
return None; }
let mut parts: Vec<&str> = module_path.split("::").collect();
if parts.is_empty() {
return None;
}
parts.pop();
if parts.is_empty() {
Some(String::new()) } else {
Some(parts.join("::"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_brace_names_simple() {
let result = parse_rust_brace_names("Foo, Bar, Baz");
assert_eq!(result.len(), 3);
assert_eq!(result[0], ("Foo".to_string(), "Foo".to_string()));
assert_eq!(result[1], ("Bar".to_string(), "Bar".to_string()));
assert_eq!(result[2], ("Baz".to_string(), "Baz".to_string()));
}
#[test]
fn test_parse_brace_names_with_alias() {
let result = parse_rust_brace_names("Foo as F, Bar");
assert_eq!(result.len(), 2);
assert_eq!(result[0], ("Foo".to_string(), "F".to_string()));
assert_eq!(result[1], ("Bar".to_string(), "Bar".to_string()));
}
#[test]
fn test_parse_brace_names_nested_path() {
let result = parse_rust_brace_names("models::Visit, types::Config");
assert_eq!(result.len(), 2);
assert_eq!(result[0], ("Visit".to_string(), "Visit".to_string()));
assert_eq!(result[1], ("Config".to_string(), "Config".to_string()));
}
#[test]
fn test_parse_brace_names_self_excluded() {
let result = parse_rust_brace_names("self, Foo, self::Bar");
assert_eq!(result.len(), 2); }
#[test]
fn test_parse_brace_names_empty() {
let result = parse_rust_brace_names(" , , ");
assert!(result.is_empty());
}
}