Skip to main content

code_ranker_graph/
finalize.rs

1//! Canonicalize a freshly-parsed structural graph: drop self-loops, deduplicate
2//! edges on `(source, target, kind)`, and prune external nodes that nothing
3//! references. Structural edges (e.g. `contains`) are preserved — they carry no
4//! flow but are kept for display and ownership.
5
6use 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    // Keep external nodes only if some edge targets them.
24    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}