#![forbid(unsafe_code)]
use std::path::Path;
use std::sync::Mutex;
use synwire_index::{XrefDirection, XrefEdge, XrefGraph, rebuild_project_xrefs, xref_query};
use synwire_storage::{
DependencyEntry, DependencyIndex, DependencyIndexError, StorageLayout, WorktreeId,
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum IndexingError {
#[error("storage error: {0}")]
Storage(#[from] synwire_storage::StorageError),
#[error("dependency index error: {0}")]
DependencyIndex(#[from] DependencyIndexError),
#[error("xref error: {0}")]
Xref(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
pub struct IndexingCoordinator {
layout: StorageLayout,
dep_index: DependencyIndex,
xref_graph: Mutex<XrefGraph>,
}
impl IndexingCoordinator {
pub fn new(layout: &StorageLayout) -> Result<Self, IndexingError> {
let dep_db_path = layout.global_dependency_db();
let dep_index = DependencyIndex::open(&dep_db_path)?;
Ok(Self {
layout: layout.clone(),
dep_index,
xref_graph: Mutex::new(XrefGraph::new()),
})
}
pub fn index_project_deps(&self, project_root: &Path) -> Result<usize, IndexingError> {
let count = self.dep_index.index_project(project_root)?;
Ok(count)
}
#[allow(clippy::significant_drop_tightening)]
pub fn rebuild_xrefs(
&self,
worktree_id: &WorktreeId,
new_edges: Vec<XrefEdge>,
) -> Result<usize, IndexingError> {
let graph_dir = self.layout.graph_dir(worktree_id);
let project_key = graph_dir.to_string_lossy().into_owned();
let mut graph = self
.xref_graph
.lock()
.map_err(|e| IndexingError::Xref(format!("xref graph lock poisoned: {e}")))?;
let count = rebuild_project_xrefs(&mut graph, &project_key, new_edges);
Ok(count)
}
#[allow(clippy::significant_drop_tightening)]
pub fn query_xrefs(
&self,
symbol: &str,
_worktree_id: &WorktreeId,
) -> Result<Vec<XrefEdge>, IndexingError> {
let graph = self
.xref_graph
.lock()
.map_err(|e| IndexingError::Xref(format!("xref graph lock poisoned: {e}")))?;
let edges = xref_query(&graph, symbol, XrefDirection::Both);
Ok(edges.into_iter().cloned().collect())
}
pub fn projects_using_dep(
&self,
dep_name: &str,
) -> Result<Vec<DependencyEntry>, IndexingError> {
let entries = self.dep_index.projects_using(dep_name)?;
Ok(entries)
}
pub fn project_dependencies(
&self,
project_path: &str,
) -> Result<Vec<DependencyEntry>, IndexingError> {
let entries = self.dep_index.dependencies_of(project_path)?;
Ok(entries)
}
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
const fn check() {
assert_send_sync::<IndexingCoordinator>();
}
let _ = check;
};
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::tempdir;
fn test_layout(dir: &Path) -> StorageLayout {
StorageLayout::with_root(dir, "synwire")
}
fn dummy_worktree() -> WorktreeId {
use synwire_storage::identity::RepoId;
WorktreeId::from_parts(
RepoId::from_string("abc123"),
"def456789012".to_owned(),
"myrepo@main".to_owned(),
)
}
#[test]
fn coordinator_indexes_cargo_project() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).expect("create dir");
std::fs::write(
project_dir.join("Cargo.toml"),
"[package]\nname = \"test\"\n\n[dependencies]\nserde = \"1\"\ntokio = \"1\"\n",
)
.expect("write Cargo.toml");
let count = coordinator
.index_project_deps(&project_dir)
.expect("index_project_deps");
assert!(count >= 2);
}
#[test]
fn coordinator_queries_deps() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let project_dir = dir.path().join("proj-a");
std::fs::create_dir_all(&project_dir).expect("create dir");
std::fs::write(
project_dir.join("Cargo.toml"),
"[package]\nname = \"a\"\n\n[dependencies]\nserde = \"1\"\n",
)
.expect("write Cargo.toml");
let _ = coordinator.index_project_deps(&project_dir).expect("index");
let projects = coordinator.projects_using_dep("serde").expect("query");
assert!(!projects.is_empty());
let deps = coordinator
.project_dependencies(&project_dir.to_string_lossy())
.expect("deps");
assert!(!deps.is_empty());
}
#[test]
fn coordinator_rebuilds_and_queries_xrefs() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let wid = dummy_worktree();
let edges = vec![
XrefEdge::new("proj_a", "proj_a::Foo", "proj_b", "proj_b::Bar"),
XrefEdge::new("proj_a", "proj_a::Baz", "proj_c", "proj_c::Qux"),
];
let count = coordinator
.rebuild_xrefs(&wid, edges)
.expect("rebuild_xrefs");
assert_eq!(count, 2);
let results = coordinator
.query_xrefs("proj_b::Bar", &wid)
.expect("query_xrefs");
assert_eq!(results.len(), 1);
assert_eq!(results[0].source_symbol, "proj_a::Foo");
}
#[test]
fn coordinator_xref_rebuild_replaces_old_edges() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let wid = dummy_worktree();
let project_key = layout.graph_dir(&wid).to_string_lossy().into_owned();
let edges1 = vec![XrefEdge::new(
&project_key,
"proj_a::OldSym",
"proj_b",
"proj_b::Target",
)];
let _ = coordinator.rebuild_xrefs(&wid, edges1).expect("rebuild 1");
let edges2 = vec![XrefEdge::new(
&project_key,
"proj_a::NewSym",
"proj_b",
"proj_b::Target",
)];
let count = coordinator.rebuild_xrefs(&wid, edges2).expect("rebuild 2");
assert_eq!(count, 1);
let old = coordinator
.query_xrefs("proj_a::OldSym", &wid)
.expect("query old");
assert!(old.is_empty());
let new = coordinator
.query_xrefs("proj_a::NewSym", &wid)
.expect("query new");
assert_eq!(new.len(), 1);
}
#[test]
fn coordinator_indexes_go_mod_project() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let project_dir = dir.path().join("go-project");
std::fs::create_dir_all(&project_dir).expect("create dir");
std::fs::write(
project_dir.join("go.mod"),
"module example.com/app\n\ngo 1.21\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)\n",
)
.expect("write go.mod");
let count = coordinator.index_project_deps(&project_dir).expect("index");
assert_eq!(count, 1);
let projects = coordinator
.projects_using_dep("github.com/gin-gonic/gin")
.expect("query");
assert!(!projects.is_empty());
}
#[test]
fn coordinator_indexes_package_json_project() {
let dir = tempdir().expect("tempdir");
let layout = test_layout(dir.path());
let coordinator = IndexingCoordinator::new(&layout).expect("new");
let project_dir = dir.path().join("node-project");
std::fs::create_dir_all(&project_dir).expect("create dir");
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"app","dependencies":{"react":"^18.0.0","axios":"^1.0.0"}}"#,
)
.expect("write package.json");
let count = coordinator.index_project_deps(&project_dir).expect("index");
assert_eq!(count, 2);
}
}