Skip to main content

icb_server/
diff.rs

1//! Diff engine for comparing two Code Property Graphs.
2//!
3//! # Overview
4//!
5//! This module loads or builds two [`CodePropertyGraph`]s (typically from
6//! two projects or two cached snapshots) and computes a symmetric
7//! difference: which functions, classes and call-edges were added, removed
8//! or stayed the same.
9//!
10//! # Output
11//!
12//! The result is a [`DiffReport`] containing two flat vectors:
13//!
14//! * `nodes` – every node present in at least one of the two graphs,
15//!   tagged with [`Status`].
16//! * `edges` – every unique `(caller, callee, kind)` tuple, also tagged
17//!   with its status.
18//!
19//! # Matching strategy
20//!
21//! * **Nodes** are matched by their `name` field (must be unique within a
22//!   graph; the caller is responsible for disambiguation).
23//! * **Edges** are matched by the triple `(source_name, target_name,
24//!   edge_kind)`.
25//!
26//! # Example
27//!
28//! ```rust,no_run
29//! use icb_server::diff::{diff_graphs, Status};
30//! use icb_graph::graph::CodePropertyGraph;
31//!
32//! let old = CodePropertyGraph::new();
33//! let new = CodePropertyGraph::new();
34//! let report = diff_graphs(&old, &new);
35//! assert!(report.nodes.is_empty());
36//! ```
37
38use icb_graph::graph::CodePropertyGraph;
39use petgraph::visit::{EdgeRef, IntoEdgeReferences};
40use serde::Serialize;
41use std::collections::HashSet;
42
43/// Status of an entity in the diff.
44#[derive(Debug, Serialize, PartialEq, Eq)]
45pub enum Status {
46    /// Present only in the new graph.
47    Added,
48    /// Present only in the old graph.
49    Removed,
50    /// Present in both graphs.
51    Unchanged,
52}
53
54/// A single node in the diff output.
55#[derive(Debug, Serialize)]
56pub struct DiffNode {
57    /// Display name of the node.
58    pub name: String,
59    /// Node kind (e.g. `Function`, `Class`).
60    pub kind: String,
61    /// Starting line in the source file.
62    pub line: usize,
63    /// Associated file path (or USR).
64    pub file: String,
65    /// Diff status.
66    pub status: Status,
67}
68
69/// A single edge in the diff output.
70#[derive(Debug, Serialize)]
71pub struct DiffEdge {
72    /// Source node name.
73    pub source: String,
74    /// Target node name.
75    pub target: String,
76    /// Edge kind (e.g. `Call`, `AstChild`).
77    pub kind: String,
78    /// Diff status.
79    pub status: Status,
80}
81
82/// Complete result of a diff operation.
83#[derive(Debug, Serialize)]
84pub struct DiffReport {
85    /// All nodes from both graphs, each tagged with its status.
86    pub nodes: Vec<DiffNode>,
87    /// All unique edges from both graphs, tagged with status.
88    pub edges: Vec<DiffEdge>,
89}
90
91/// Compute the symmetric difference between two graphs.
92///
93/// Nodes are matched by their `name` field.  Edges are matched by the
94/// ordered triple `(source_name, target_name, edge_kind)`.
95pub fn diff_graphs(old: &CodePropertyGraph, new: &CodePropertyGraph) -> DiffReport {
96    let mut report = DiffReport {
97        nodes: Vec::new(),
98        edges: Vec::new(),
99    };
100
101    let old_names: HashSet<String> = old
102        .graph
103        .node_weights()
104        .map(|n| n.name.clone().unwrap_or_default())
105        .collect();
106    let new_names: HashSet<String> = new
107        .graph
108        .node_weights()
109        .map(|n| n.name.clone().unwrap_or_default())
110        .collect();
111
112    let all_names: HashSet<&str> = old_names
113        .iter()
114        .chain(new_names.iter())
115        .map(|s| s.as_str())
116        .collect();
117
118    for name in all_names {
119        let in_old = old_names.contains(name);
120        let in_new = new_names.contains(name);
121        let (status, node_ref) = if in_old && in_new {
122            let node = old
123                .graph
124                .node_weights()
125                .find(|n| n.name.as_deref() == Some(name))
126                .unwrap();
127            (Status::Unchanged, node)
128        } else if in_old {
129            let node = old
130                .graph
131                .node_weights()
132                .find(|n| n.name.as_deref() == Some(name))
133                .unwrap();
134            (Status::Removed, node)
135        } else {
136            let node = new
137                .graph
138                .node_weights()
139                .find(|n| n.name.as_deref() == Some(name))
140                .unwrap();
141            (Status::Added, node)
142        };
143
144        report.nodes.push(DiffNode {
145            name: name.to_string(),
146            kind: format!("{:?}", node_ref.kind),
147            line: node_ref.start_line,
148            file: node_ref.usr.clone().unwrap_or_default(),
149            status,
150        });
151    }
152
153    let mut old_edge_triples: HashSet<(String, String, String)> = HashSet::new();
154    let mut new_edge_triples: HashSet<(String, String, String)> = HashSet::new();
155
156    for edge_ref in old.graph.edge_references() {
157        let src = old.graph[edge_ref.source()]
158            .name
159            .clone()
160            .unwrap_or_default();
161        let tgt = old.graph[edge_ref.target()]
162            .name
163            .clone()
164            .unwrap_or_default();
165        let kind = format!("{:?}", edge_ref.weight());
166        old_edge_triples.insert((src, tgt, kind));
167    }
168    for edge_ref in new.graph.edge_references() {
169        let src = new.graph[edge_ref.source()]
170            .name
171            .clone()
172            .unwrap_or_default();
173        let tgt = new.graph[edge_ref.target()]
174            .name
175            .clone()
176            .unwrap_or_default();
177        let kind = format!("{:?}", edge_ref.weight());
178        new_edge_triples.insert((src, tgt, kind));
179    }
180
181    for (src, tgt, kind) in new_edge_triples.difference(&old_edge_triples) {
182        report.edges.push(DiffEdge {
183            source: src.clone(),
184            target: tgt.clone(),
185            kind: kind.clone(),
186            status: Status::Added,
187        });
188    }
189    for (src, tgt, kind) in old_edge_triples.difference(&new_edge_triples) {
190        report.edges.push(DiffEdge {
191            source: src.clone(),
192            target: tgt.clone(),
193            kind: kind.clone(),
194            status: Status::Removed,
195        });
196    }
197
198    report
199}