use super::rust::find_references_in_file;
use crate::error::{Result, SpliceError};
use crate::ingest::imports::extract_rust_imports;
use crate::ingest::rust::RustSymbol;
use ropey::Rope;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
struct Reexport {
reexporting_module: String,
_reexported_name: String,
_replaced_module: String,
_replaced_name: String,
}
fn build_reexport_map(
workspace_root: &Path,
rust_files: &[PathBuf],
) -> Result<std::collections::HashMap<(String, String), Vec<Reexport>>> {
let mut reexport_map: std::collections::HashMap<(String, String), Vec<Reexport>> =
std::collections::HashMap::new();
for file_path in rust_files {
let source = match std::fs::read(file_path) {
Ok(s) => s,
Err(_) => continue,
};
let imports = match extract_rust_imports(file_path, &source) {
Ok(i) => i,
Err(_) => continue,
};
let module_path = match module_path_from_file(workspace_root, file_path) {
Ok(m) => m,
Err(_) => continue,
};
for import in imports {
if !import.is_reexport {
continue;
}
let imported_module = import.path.join("::");
for name in &import.imported_names {
if name == "*" {
continue;
}
let reexport = Reexport {
reexporting_module: module_path.clone(),
_reexported_name: name.clone(),
_replaced_module: imported_module.clone(),
_replaced_name: name.clone(),
};
let key = (imported_module.clone(), name.clone());
reexport_map.entry(key).or_default().push(reexport);
}
}
}
Ok(reexport_map)
}
fn module_path_from_file(workspace_root: &Path, file_path: &Path) -> Result<String> {
let relative = file_path
.strip_prefix(workspace_root)
.map_err(|_| SpliceError::Other("File not in workspace".to_string()))?;
let path_str = relative
.to_str()
.ok_or_else(|| SpliceError::Other("Invalid UTF-8 in path".to_string()))?;
let module_path = path_str
.trim_end_matches(".rs")
.replace("/", "::")
.replace("\\", "::");
let module_path = module_path.replace("::mod", "");
let module_path = if module_path.starts_with("crate::") {
module_path
} else if module_path.starts_with("lib::") || module_path.starts_with("src::") {
let rest = module_path
.split("::")
.skip(1)
.collect::<Vec<_>>()
.join("::");
format!("crate::{}", rest)
} else {
format!("crate::{}", module_path)
};
Ok(module_path)
}
fn module_reexports_symbol(
module_path: &str,
target_module: &str,
target_symbol: &str,
reexport_map: &std::collections::HashMap<(String, String), Vec<Reexport>>,
) -> bool {
let key = (target_module.to_string(), target_symbol.to_string());
if let Some(reexports) = reexport_map.get(&key) {
for reexport in reexports {
if reexport.reexporting_module == module_path {
return true;
}
}
}
false
}
pub(crate) fn find_cross_file_references(
definition_file: &Path,
target_symbol: &RustSymbol,
) -> Result<(Vec<super::Reference>, bool)> {
let mut all_references = Vec::new();
let mut has_glob_ambiguity = false;
let workspace_root = find_workspace_root(definition_file)?;
let rust_files = find_all_rust_files(&workspace_root)?;
let reexport_map = match build_reexport_map(&workspace_root, &rust_files) {
Ok(m) => m,
Err(e) => {
eprintln!("Warning: failed to build re-export map: {}", e);
std::collections::HashMap::new()
}
};
let target_module = &target_symbol.module_path;
for file_path in rust_files {
if file_path == definition_file {
continue;
}
let source = match std::fs::read(&file_path) {
Ok(s) => s,
Err(_) => continue, };
let imports = match extract_rust_imports(&file_path, &source) {
Ok(i) => i,
Err(_) => continue, };
let (matches, has_glob) =
import_matches_module(&imports, target_module, &target_symbol.name);
let matches_reexport =
check_reexport_matches(&imports, target_module, &target_symbol.name, &reexport_map);
if has_glob {
has_glob_ambiguity = true;
}
if matches || matches_reexport {
let rope = Rope::from_str(std::str::from_utf8(&source)?);
let refs = find_references_in_file(&source, &rope, target_symbol, &file_path)?;
all_references.extend(refs);
}
}
Ok((all_references, has_glob_ambiguity))
}
fn check_reexport_matches(
imports: &[crate::ingest::imports::ImportFact],
target_module: &str,
target_symbol: &str,
reexport_map: &std::collections::HashMap<(String, String), Vec<Reexport>>,
) -> bool {
for import in imports {
let imported_module = import.path.join("::");
for name in &import.imported_names {
if name == "*" {
if module_reexports_symbol(
&imported_module,
target_module,
target_symbol,
reexport_map,
) {
return true;
}
} else if name == target_symbol {
if module_reexports_symbol(
&imported_module,
target_module,
target_symbol,
reexport_map,
) {
return true;
}
}
}
}
false
}
pub(crate) fn find_workspace_root(start_path: &Path) -> Result<PathBuf> {
const MARKERS: &[&str] = &[
"Cargo.toml",
"pyproject.toml",
"package.json",
"go.mod",
"pom.xml",
"build.gradle",
"setup.py",
];
let boundaries = build_workspace_boundary_set(start_path);
let mut current = start_path.parent();
while let Some(dir) = current {
if boundaries.contains(dir) {
break;
}
for marker in MARKERS {
if dir.join(marker).exists() {
return Ok(dir.to_path_buf());
}
}
current = dir.parent();
}
Err(SpliceError::Other(format!(
"No project marker (Cargo.toml, pyproject.toml, package.json, go.mod, pom.xml, build.gradle, setup.py) found in any ancestor of {} within $HOME or before /tmp",
start_path.display()
)))
}
fn build_workspace_boundary_set(file_path: &Path) -> std::collections::HashSet<PathBuf> {
let mut set = std::collections::HashSet::new();
set.insert(PathBuf::from("/"));
set.insert(PathBuf::from("/tmp"));
if let Some(tmpdir) = std::env::var_os("TMPDIR") {
set.insert(PathBuf::from(tmpdir));
}
if let Some(home) = std::env::var_os("HOME") {
let home_path = PathBuf::from(&home);
if !file_path.starts_with(&home_path) {
set.insert(home_path);
}
}
set
}
fn find_all_rust_files(workspace_root: &Path) -> Result<Vec<PathBuf>> {
let mut rust_files = Vec::new();
fn visit_dirs(dir: &Path, rust_files: &mut Vec<PathBuf>) -> Result<()> {
if dir
.file_name()
.map(|n| n.to_str().unwrap_or(""))
.unwrap_or("")
== "target"
{
return Ok(());
}
if dir
.file_name()
.map(|n| n.to_str().unwrap_or(""))
.unwrap_or("")
== ".git"
{
return Ok(());
}
if dir
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.starts_with('.'))
.unwrap_or(false)
{
return Ok(());
}
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()), };
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
visit_dirs(&path, rust_files)?;
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
rust_files.push(path);
}
}
Ok(())
}
visit_dirs(workspace_root, &mut rust_files)?;
Ok(rust_files)
}
fn import_matches_module(
imports: &[crate::ingest::imports::ImportFact],
target_module: &str,
target_symbol_name: &str,
) -> (bool, bool) {
let mut matches = false;
let mut has_glob = false;
for import in imports {
if import.is_glob {
has_glob = true;
let import_path = import.path.join("::");
if import_path_matches_target(&import_path, target_module) {
matches = true;
}
} else {
if import
.imported_names
.contains(&target_symbol_name.to_string())
{
let import_path = import.path.join("::");
if import_path_matches_target(&import_path, target_module) {
matches = true;
}
}
}
}
(matches, has_glob)
}
pub(crate) fn import_path_matches_target(import_path: &str, target_module: &str) -> bool {
if import_path == target_module {
return true;
}
if target_module.starts_with(&format!("{}::", import_path)) {
return true;
}
if import_path.starts_with(&format!("{}::", target_module)) {
return true;
}
false
}