synwire_daemon/
indexing.rs1#![forbid(unsafe_code)]
9
10use std::path::Path;
11use std::sync::Mutex;
12
13use synwire_index::{XrefDirection, XrefEdge, XrefGraph, rebuild_project_xrefs, xref_query};
14use synwire_storage::{
15 DependencyEntry, DependencyIndex, DependencyIndexError, StorageLayout, WorktreeId,
16};
17
18#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum IndexingError {
22 #[error("storage error: {0}")]
24 Storage(#[from] synwire_storage::StorageError),
25 #[error("dependency index error: {0}")]
27 DependencyIndex(#[from] DependencyIndexError),
28 #[error("xref error: {0}")]
30 Xref(String),
31 #[error("io error: {0}")]
33 Io(#[from] std::io::Error),
34}
35
36pub struct IndexingCoordinator {
49 layout: StorageLayout,
51 dep_index: DependencyIndex,
53 xref_graph: Mutex<XrefGraph>,
55}
56
57impl IndexingCoordinator {
58 pub fn new(layout: &StorageLayout) -> Result<Self, IndexingError> {
69 let dep_db_path = layout.global_dependency_db();
70 let dep_index = DependencyIndex::open(&dep_db_path)?;
71
72 Ok(Self {
73 layout: layout.clone(),
74 dep_index,
75 xref_graph: Mutex::new(XrefGraph::new()),
76 })
77 }
78
79 pub fn index_project_deps(&self, project_root: &Path) -> Result<usize, IndexingError> {
91 let count = self.dep_index.index_project(project_root)?;
92 Ok(count)
93 }
94
95 #[allow(clippy::significant_drop_tightening)]
108 pub fn rebuild_xrefs(
109 &self,
110 worktree_id: &WorktreeId,
111 new_edges: Vec<XrefEdge>,
112 ) -> Result<usize, IndexingError> {
113 let graph_dir = self.layout.graph_dir(worktree_id);
114 let project_key = graph_dir.to_string_lossy().into_owned();
115
116 let mut graph = self
117 .xref_graph
118 .lock()
119 .map_err(|e| IndexingError::Xref(format!("xref graph lock poisoned: {e}")))?;
120
121 let count = rebuild_project_xrefs(&mut graph, &project_key, new_edges);
122 Ok(count)
123 }
124
125 #[allow(clippy::significant_drop_tightening)]
135 pub fn query_xrefs(
136 &self,
137 symbol: &str,
138 _worktree_id: &WorktreeId,
139 ) -> Result<Vec<XrefEdge>, IndexingError> {
140 let graph = self
141 .xref_graph
142 .lock()
143 .map_err(|e| IndexingError::Xref(format!("xref graph lock poisoned: {e}")))?;
144
145 let edges = xref_query(&graph, symbol, XrefDirection::Both);
146 Ok(edges.into_iter().cloned().collect())
147 }
148
149 pub fn projects_using_dep(
157 &self,
158 dep_name: &str,
159 ) -> Result<Vec<DependencyEntry>, IndexingError> {
160 let entries = self.dep_index.projects_using(dep_name)?;
161 Ok(entries)
162 }
163
164 pub fn project_dependencies(
172 &self,
173 project_path: &str,
174 ) -> Result<Vec<DependencyEntry>, IndexingError> {
175 let entries = self.dep_index.dependencies_of(project_path)?;
176 Ok(entries)
177 }
178}
179
180const _: () = {
182 const fn assert_send_sync<T: Send + Sync>() {}
183 const fn check() {
184 assert_send_sync::<IndexingCoordinator>();
185 }
186 let _ = check;
187};
188
189#[cfg(test)]
190#[allow(clippy::expect_used, clippy::unwrap_used)]
191mod tests {
192 use super::*;
193 use tempfile::tempdir;
194
195 fn test_layout(dir: &Path) -> StorageLayout {
196 StorageLayout::with_root(dir, "synwire")
197 }
198
199 fn dummy_worktree() -> WorktreeId {
200 use synwire_storage::identity::RepoId;
201 WorktreeId::from_parts(
202 RepoId::from_string("abc123"),
203 "def456789012".to_owned(),
204 "myrepo@main".to_owned(),
205 )
206 }
207
208 #[test]
209 fn coordinator_indexes_cargo_project() {
210 let dir = tempdir().expect("tempdir");
211 let layout = test_layout(dir.path());
212 let coordinator = IndexingCoordinator::new(&layout).expect("new");
213
214 let project_dir = dir.path().join("my-project");
216 std::fs::create_dir_all(&project_dir).expect("create dir");
217 std::fs::write(
218 project_dir.join("Cargo.toml"),
219 "[package]\nname = \"test\"\n\n[dependencies]\nserde = \"1\"\ntokio = \"1\"\n",
220 )
221 .expect("write Cargo.toml");
222
223 let count = coordinator
224 .index_project_deps(&project_dir)
225 .expect("index_project_deps");
226 assert!(count >= 2);
227 }
228
229 #[test]
230 fn coordinator_queries_deps() {
231 let dir = tempdir().expect("tempdir");
232 let layout = test_layout(dir.path());
233 let coordinator = IndexingCoordinator::new(&layout).expect("new");
234
235 let project_dir = dir.path().join("proj-a");
236 std::fs::create_dir_all(&project_dir).expect("create dir");
237 std::fs::write(
238 project_dir.join("Cargo.toml"),
239 "[package]\nname = \"a\"\n\n[dependencies]\nserde = \"1\"\n",
240 )
241 .expect("write Cargo.toml");
242
243 let _ = coordinator.index_project_deps(&project_dir).expect("index");
244
245 let projects = coordinator.projects_using_dep("serde").expect("query");
246 assert!(!projects.is_empty());
247
248 let deps = coordinator
249 .project_dependencies(&project_dir.to_string_lossy())
250 .expect("deps");
251 assert!(!deps.is_empty());
252 }
253
254 #[test]
255 fn coordinator_rebuilds_and_queries_xrefs() {
256 let dir = tempdir().expect("tempdir");
257 let layout = test_layout(dir.path());
258 let coordinator = IndexingCoordinator::new(&layout).expect("new");
259 let wid = dummy_worktree();
260
261 let edges = vec![
262 XrefEdge::new("proj_a", "proj_a::Foo", "proj_b", "proj_b::Bar"),
263 XrefEdge::new("proj_a", "proj_a::Baz", "proj_c", "proj_c::Qux"),
264 ];
265
266 let count = coordinator
267 .rebuild_xrefs(&wid, edges)
268 .expect("rebuild_xrefs");
269 assert_eq!(count, 2);
270
271 let results = coordinator
272 .query_xrefs("proj_b::Bar", &wid)
273 .expect("query_xrefs");
274 assert_eq!(results.len(), 1);
275 assert_eq!(results[0].source_symbol, "proj_a::Foo");
276 }
277
278 #[test]
279 fn coordinator_xref_rebuild_replaces_old_edges() {
280 let dir = tempdir().expect("tempdir");
281 let layout = test_layout(dir.path());
282 let coordinator = IndexingCoordinator::new(&layout).expect("new");
283 let wid = dummy_worktree();
284
285 let project_key = layout.graph_dir(&wid).to_string_lossy().into_owned();
289
290 let edges1 = vec![XrefEdge::new(
292 &project_key,
293 "proj_a::OldSym",
294 "proj_b",
295 "proj_b::Target",
296 )];
297 let _ = coordinator.rebuild_xrefs(&wid, edges1).expect("rebuild 1");
298
299 let edges2 = vec![XrefEdge::new(
301 &project_key,
302 "proj_a::NewSym",
303 "proj_b",
304 "proj_b::Target",
305 )];
306 let count = coordinator.rebuild_xrefs(&wid, edges2).expect("rebuild 2");
307 assert_eq!(count, 1);
308
309 let old = coordinator
311 .query_xrefs("proj_a::OldSym", &wid)
312 .expect("query old");
313 assert!(old.is_empty());
314
315 let new = coordinator
317 .query_xrefs("proj_a::NewSym", &wid)
318 .expect("query new");
319 assert_eq!(new.len(), 1);
320 }
321
322 #[test]
323 fn coordinator_indexes_go_mod_project() {
324 let dir = tempdir().expect("tempdir");
325 let layout = test_layout(dir.path());
326 let coordinator = IndexingCoordinator::new(&layout).expect("new");
327
328 let project_dir = dir.path().join("go-project");
329 std::fs::create_dir_all(&project_dir).expect("create dir");
330 std::fs::write(
331 project_dir.join("go.mod"),
332 "module example.com/app\n\ngo 1.21\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)\n",
333 )
334 .expect("write go.mod");
335
336 let count = coordinator.index_project_deps(&project_dir).expect("index");
337 assert_eq!(count, 1);
338
339 let projects = coordinator
340 .projects_using_dep("github.com/gin-gonic/gin")
341 .expect("query");
342 assert!(!projects.is_empty());
343 }
344
345 #[test]
346 fn coordinator_indexes_package_json_project() {
347 let dir = tempdir().expect("tempdir");
348 let layout = test_layout(dir.path());
349 let coordinator = IndexingCoordinator::new(&layout).expect("new");
350
351 let project_dir = dir.path().join("node-project");
352 std::fs::create_dir_all(&project_dir).expect("create dir");
353 std::fs::write(
354 project_dir.join("package.json"),
355 r#"{"name":"app","dependencies":{"react":"^18.0.0","axios":"^1.0.0"}}"#,
356 )
357 .expect("write package.json");
358
359 let count = coordinator.index_project_deps(&project_dir).expect("index");
360 assert_eq!(count, 2);
361 }
362}