Skip to main content

synwire_index/
xref.rs

1//! Cross-project symbol reference graph.
2//!
3//! During indexing, resolves imports against locally-indexed projects'
4//! symbol tables to produce inter-project edges.  Stored in
5//! `StorageLayout.global_dependency_db()` (shared `SQLite`).
6
7/// A directed edge in the cross-project reference graph.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct XrefEdge {
11    /// Source project root.
12    pub source_project: String,
13    /// Source symbol (qualified name).
14    pub source_symbol: String,
15    /// Target project root.
16    pub target_project: String,
17    /// Target symbol.
18    pub target_symbol: String,
19    /// Whether this edge is stale (source or target re-indexed).
20    pub is_stale: bool,
21}
22
23impl XrefEdge {
24    /// Construct a non-stale [`XrefEdge`].
25    ///
26    /// # Example
27    ///
28    /// ```rust
29    /// use synwire_index::XrefEdge;
30    ///
31    /// let edge = XrefEdge::new("proj_a", "proj_a::Foo", "proj_b", "proj_b::Bar");
32    /// assert!(!edge.is_stale);
33    /// ```
34    #[must_use]
35    pub fn new(
36        source_project: impl Into<String>,
37        source_symbol: impl Into<String>,
38        target_project: impl Into<String>,
39        target_symbol: impl Into<String>,
40    ) -> Self {
41        Self {
42            source_project: source_project.into(),
43            source_symbol: source_symbol.into(),
44            target_project: target_project.into(),
45            target_symbol: target_symbol.into(),
46            is_stale: false,
47        }
48    }
49}
50
51/// Direction for [`XrefGraph::xref_query`] lookups.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum XrefDirection {
55    /// References made by the symbol (outgoing edges where `source_symbol` matches).
56    Outgoing,
57    /// References to the symbol (incoming edges where `target_symbol` matches).
58    Incoming,
59    /// Both outgoing and incoming edges.
60    Both,
61}
62
63/// In-memory cross-project symbol reference graph.
64///
65/// Edges can be added incrementally; stale edges are marked and pruned when a
66/// project is re-indexed via [`rebuild_project_xrefs`].
67pub struct XrefGraph {
68    edges: Vec<XrefEdge>,
69}
70
71impl XrefGraph {
72    /// Create a new, empty cross-project reference graph.
73    #[must_use]
74    pub const fn new() -> Self {
75        Self { edges: Vec::new() }
76    }
77
78    /// Add a cross-project reference edge.
79    pub fn add_edge(&mut self, edge: XrefEdge) {
80        self.edges.push(edge);
81    }
82
83    /// Mark all edges whose `source_project` or `target_project` matches
84    /// `project` as stale.
85    pub fn mark_stale(&mut self, project: &str) {
86        for edge in &mut self.edges {
87            if edge.source_project == project || edge.target_project == project {
88                edge.is_stale = true;
89            }
90        }
91    }
92
93    /// Remove all edges that have been marked stale.
94    pub fn prune_stale(&mut self) {
95        self.edges.retain(|e| !e.is_stale);
96    }
97
98    /// Query cross-project references for a symbol.
99    ///
100    /// Only non-stale edges are returned.
101    #[must_use]
102    pub fn xref_query(&self, symbol: &str, direction: XrefDirection) -> Vec<&XrefEdge> {
103        self.edges
104            .iter()
105            .filter(|e| !e.is_stale)
106            .filter(|e| match direction {
107                XrefDirection::Outgoing => e.source_symbol == symbol,
108                XrefDirection::Incoming => e.target_symbol == symbol,
109                XrefDirection::Both => e.source_symbol == symbol || e.target_symbol == symbol,
110            })
111            .collect()
112    }
113
114    /// Return the total number of edges (including stale edges).
115    #[must_use]
116    pub const fn len(&self) -> usize {
117        self.edges.len()
118    }
119
120    /// Return `true` if the graph contains no edges.
121    #[must_use]
122    pub const fn is_empty(&self) -> bool {
123        self.edges.is_empty()
124    }
125}
126
127impl Default for XrefGraph {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133/// Query cross-project references using a shared xref graph.
134///
135/// This is the main entry point for xref lookup from external crates.
136///
137/// # Example
138///
139/// ```rust
140/// use synwire_index::{XrefGraph, XrefEdge, XrefDirection, xref_query};
141///
142/// let mut graph = XrefGraph::new();
143/// graph.add_edge(XrefEdge::new("proj_a", "proj_a::Foo", "proj_b", "proj_b::Bar"));
144///
145/// let results = xref_query(&graph, "proj_b::Bar", XrefDirection::Incoming);
146/// assert_eq!(results.len(), 1);
147/// ```
148pub fn xref_query<'a>(
149    graph: &'a XrefGraph,
150    symbol: &str,
151    direction: XrefDirection,
152) -> Vec<&'a XrefEdge> {
153    graph.xref_query(symbol, direction)
154}
155
156/// Invalidate and rebuild xrefs for a given project.
157///
158/// Marks all edges from or to `project` as stale, prunes them, then inserts
159/// `new_edges`.  Returns the number of new edges added.
160///
161/// # Example
162///
163/// ```rust
164/// use synwire_index::{XrefGraph, XrefEdge, XrefDirection, rebuild_project_xrefs};
165///
166/// let mut graph = XrefGraph::new();
167/// graph.add_edge(XrefEdge::new("proj_a", "fn_old", "proj_b", "fn_b"));
168///
169/// let new_edges = vec![XrefEdge::new("proj_a", "fn_new", "proj_b", "fn_b")];
170/// let added = rebuild_project_xrefs(&mut graph, "proj_a", new_edges);
171/// assert_eq!(added, 1);
172/// assert!(graph.xref_query("fn_old", XrefDirection::Outgoing).is_empty());
173/// ```
174pub fn rebuild_project_xrefs(
175    graph: &mut XrefGraph,
176    project: &str,
177    new_edges: Vec<XrefEdge>,
178) -> usize {
179    graph.mark_stale(project);
180    graph.prune_stale();
181    let count = new_edges.len();
182    for edge in new_edges {
183        graph.add_edge(edge);
184    }
185    count
186}
187
188// Ensure the public API is `Send + Sync`.
189const _: () = {
190    const fn assert_send_sync<T: Send + Sync>() {}
191    const fn check() {
192        assert_send_sync::<XrefGraph>();
193    }
194    let _ = check;
195};
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn xref_query_finds_cross_project_callers() {
203        let mut graph = XrefGraph::new();
204        graph.add_edge(XrefEdge {
205            source_project: "/home/user/proj-a".to_owned(),
206            source_symbol: "proj_a::fetch_user".to_owned(),
207            target_project: "/home/user/proj-b".to_owned(),
208            target_symbol: "proj_b::User".to_owned(),
209            is_stale: false,
210        });
211
212        let results = graph.xref_query("proj_b::User", XrefDirection::Incoming);
213        assert_eq!(results.len(), 1);
214        assert_eq!(results[0].source_symbol, "proj_a::fetch_user");
215    }
216
217    #[test]
218    fn incremental_xref_matches_full() {
219        let mut graph = XrefGraph::new();
220        graph.add_edge(XrefEdge {
221            source_project: "proj_a".to_owned(),
222            source_symbol: "fn_old".to_owned(),
223            target_project: "proj_b".to_owned(),
224            target_symbol: "fn_b".to_owned(),
225            is_stale: false,
226        });
227
228        let new_edges = vec![XrefEdge {
229            source_project: "proj_a".to_owned(),
230            source_symbol: "fn_new".to_owned(),
231            target_project: "proj_b".to_owned(),
232            target_symbol: "fn_b".to_owned(),
233            is_stale: false,
234        }];
235        let added = rebuild_project_xrefs(&mut graph, "proj_a", new_edges);
236
237        assert_eq!(added, 1);
238        assert!(
239            graph
240                .xref_query("fn_old", XrefDirection::Outgoing)
241                .is_empty()
242        );
243        assert!(
244            !graph
245                .xref_query("fn_new", XrefDirection::Outgoing)
246                .is_empty()
247        );
248    }
249
250    #[test]
251    fn stale_edges_excluded_from_query() {
252        let mut graph = XrefGraph::new();
253        graph.add_edge(XrefEdge {
254            source_project: "proj_a".to_owned(),
255            source_symbol: "sym_a".to_owned(),
256            target_project: "proj_b".to_owned(),
257            target_symbol: "sym_b".to_owned(),
258            is_stale: false,
259        });
260        graph.mark_stale("proj_a");
261
262        let results = graph.xref_query("sym_a", XrefDirection::Outgoing);
263        assert!(results.is_empty());
264    }
265
266    #[test]
267    fn prune_stale_removes_only_stale() {
268        let mut graph = XrefGraph::new();
269        graph.add_edge(XrefEdge {
270            source_project: "proj_a".to_owned(),
271            source_symbol: "sym_a".to_owned(),
272            target_project: "proj_b".to_owned(),
273            target_symbol: "sym_b".to_owned(),
274            is_stale: false,
275        });
276        graph.add_edge(XrefEdge {
277            source_project: "proj_c".to_owned(),
278            source_symbol: "sym_c".to_owned(),
279            target_project: "proj_b".to_owned(),
280            target_symbol: "sym_b".to_owned(),
281            is_stale: false,
282        });
283        graph.mark_stale("proj_a");
284        graph.prune_stale();
285
286        assert_eq!(graph.len(), 1);
287        assert!(
288            !graph
289                .xref_query("sym_c", XrefDirection::Outgoing)
290                .is_empty()
291        );
292    }
293
294    #[test]
295    fn both_direction_returns_outgoing_and_incoming() {
296        let mut graph = XrefGraph::new();
297        graph.add_edge(XrefEdge {
298            source_project: "a".to_owned(),
299            source_symbol: "target_sym".to_owned(),
300            target_project: "b".to_owned(),
301            target_symbol: "other".to_owned(),
302            is_stale: false,
303        });
304        graph.add_edge(XrefEdge {
305            source_project: "c".to_owned(),
306            source_symbol: "caller".to_owned(),
307            target_project: "d".to_owned(),
308            target_symbol: "target_sym".to_owned(),
309            is_stale: false,
310        });
311
312        let results = graph.xref_query("target_sym", XrefDirection::Both);
313        assert_eq!(results.len(), 2);
314    }
315
316    #[test]
317    fn standalone_xref_query_fn_delegates_to_method() {
318        let mut graph = XrefGraph::new();
319        graph.add_edge(XrefEdge {
320            source_project: "a".to_owned(),
321            source_symbol: "sym".to_owned(),
322            target_project: "b".to_owned(),
323            target_symbol: "tgt".to_owned(),
324            is_stale: false,
325        });
326
327        let via_fn = xref_query(&graph, "sym", XrefDirection::Outgoing);
328        let via_method = graph.xref_query("sym", XrefDirection::Outgoing);
329        assert_eq!(via_fn.len(), via_method.len());
330    }
331}