1#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct XrefEdge {
11 pub source_project: String,
13 pub source_symbol: String,
15 pub target_project: String,
17 pub target_symbol: String,
19 pub is_stale: bool,
21}
22
23impl XrefEdge {
24 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum XrefDirection {
55 Outgoing,
57 Incoming,
59 Both,
61}
62
63pub struct XrefGraph {
68 edges: Vec<XrefEdge>,
69}
70
71impl XrefGraph {
72 #[must_use]
74 pub const fn new() -> Self {
75 Self { edges: Vec::new() }
76 }
77
78 pub fn add_edge(&mut self, edge: XrefEdge) {
80 self.edges.push(edge);
81 }
82
83 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 pub fn prune_stale(&mut self) {
95 self.edges.retain(|e| !e.is_stale);
96 }
97
98 #[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 #[must_use]
116 pub const fn len(&self) -> usize {
117 self.edges.len()
118 }
119
120 #[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
133pub 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
156pub 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
188const _: () = {
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}