code_ranker_graph/
finalize.rs1use code_ranker_plugin_api::graph::Graph;
7use std::collections::HashSet;
8
9use crate::attrs::is_external;
10
11pub fn finalize_graph(graph: &mut Graph) {
12 let mut seen: HashSet<(String, String, String)> = HashSet::new();
13 let mut edges = Vec::with_capacity(graph.edges.len());
14 for e in std::mem::take(&mut graph.edges) {
15 if e.source == e.target {
16 continue;
17 }
18 if seen.insert((e.source.clone(), e.target.clone(), e.kind.clone())) {
19 edges.push(e);
20 }
21 }
22
23 let referenced: HashSet<&str> = edges.iter().map(|e| e.target.as_str()).collect();
25 graph
26 .nodes
27 .retain(|n| !is_external(n) || referenced.contains(n.id.as_str()));
28
29 graph.nodes.sort_by(|a, b| a.id.cmp(&b.id));
30 edges.sort_by(|a, b| {
31 a.source
32 .cmp(&b.source)
33 .then(a.target.cmp(&b.target))
34 .then(a.kind.cmp(&b.kind))
35 });
36 graph.edges = edges;
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use code_ranker_plugin_api::{edge::Edge, node::Node};
43
44 fn edge(from: &str, to: &str) -> Edge {
45 Edge {
46 source: from.into(),
47 target: to.into(),
48 kind: "uses".into(),
49 line: None,
50 attrs: Default::default(),
51 }
52 }
53
54 #[test]
55 fn dedups_and_drops_self_loops_and_unused_externals() {
56 let mut g = Graph {
57 nodes: vec![
58 Node {
59 id: "a".into(),
60 kind: "file".into(),
61 name: "a".into(),
62 parent: None,
63 attrs: Default::default(),
64 },
65 Node {
66 id: "ext:unused".into(),
67 kind: "external".into(),
68 name: "unused".into(),
69 parent: None,
70 attrs: Default::default(),
71 },
72 ],
73 edges: vec![edge("a", "b"), edge("a", "b"), edge("a", "a")],
74 };
75 finalize_graph(&mut g);
76 assert_eq!(g.edges.len(), 1);
77 assert!(g.nodes.iter().all(|n| n.id != "ext:unused"));
78 }
79}