1use crate::{GraphStore, Located, Reference, Symbol, SymbolKind};
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15pub struct GraphDiff {
16 pub added_symbols: Vec<Located<Symbol>>,
18 pub removed_symbols: Vec<Located<Symbol>>,
20 pub added_references: Vec<Located<Reference>>,
22 pub removed_references: Vec<Located<Reference>>,
24}
25
26impl GraphDiff {
27 pub fn is_empty(&self) -> bool {
29 self.added_symbols.is_empty()
30 && self.removed_symbols.is_empty()
31 && self.added_references.is_empty()
32 && self.removed_references.is_empty()
33 }
34}
35
36type SymbolKey = (String, String, SymbolKind);
37type ReferenceKey = (String, Option<String>, String);
38
39fn symbol_key(l: &Located<Symbol>) -> SymbolKey {
40 (l.path.clone(), l.item.name.clone(), l.item.kind)
41}
42
43fn reference_key(l: &Located<Reference>) -> ReferenceKey {
44 (l.path.clone(), l.item.from.clone(), l.item.name.clone())
45}
46
47pub fn diff(a: &dyn GraphStore, b: &dyn GraphStore) -> GraphDiff {
49 let (a_syms, b_syms) = (a.all_symbols(), b.all_symbols());
50 let (a_refs, b_refs) = (a.all_references(), b.all_references());
51
52 let a_sym_keys: HashSet<SymbolKey> = a_syms.iter().map(symbol_key).collect();
53 let b_sym_keys: HashSet<SymbolKey> = b_syms.iter().map(symbol_key).collect();
54 let a_ref_keys: HashSet<ReferenceKey> = a_refs.iter().map(reference_key).collect();
55 let b_ref_keys: HashSet<ReferenceKey> = b_refs.iter().map(reference_key).collect();
56
57 GraphDiff {
58 added_symbols: dedup_by_key(b_syms, |l| !a_sym_keys.contains(&symbol_key(l)), symbol_key),
59 removed_symbols: dedup_by_key(a_syms, |l| !b_sym_keys.contains(&symbol_key(l)), symbol_key),
60 added_references: dedup_by_key(
61 b_refs,
62 |l| !a_ref_keys.contains(&reference_key(l)),
63 reference_key,
64 ),
65 removed_references: dedup_by_key(
66 a_refs,
67 |l| !b_ref_keys.contains(&reference_key(l)),
68 reference_key,
69 ),
70 }
71}
72
73fn dedup_by_key<T, K: std::hash::Hash + Eq>(
75 items: Vec<T>,
76 keep: impl Fn(&T) -> bool,
77 key: impl Fn(&T) -> K,
78) -> Vec<T> {
79 let mut seen = HashSet::new();
80 items
81 .into_iter()
82 .filter(|it| keep(it) && seen.insert(key(it)))
83 .collect()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::{InMemoryGraphStore, build_rust};
90
91 fn store(files: &[(&str, &str)]) -> InMemoryGraphStore {
92 let mut s = InMemoryGraphStore::new();
93 for (path, src) in files {
94 s.insert(path, build_rust(src));
95 }
96 s
97 }
98
99 #[test]
100 fn identical_views_have_no_diff() {
101 let a = store(&[("lib.rs", "fn a() {}\nfn b() { a(); }")]);
102 let b = store(&[("lib.rs", "fn a() {}\nfn b() { a(); }")]);
103 assert!(diff(&a, &b).is_empty());
104 }
105
106 #[test]
107 fn reports_added_and_removed_symbols() {
108 let a = store(&[("lib.rs", "fn keep() {}\nfn gone() {}")]);
109 let b = store(&[("lib.rs", "fn keep() {}\nfn fresh() {}")]);
110 let d = diff(&a, &b);
111
112 let added: Vec<&str> = d
113 .added_symbols
114 .iter()
115 .map(|l| l.item.name.as_str())
116 .collect();
117 let removed: Vec<&str> = d
118 .removed_symbols
119 .iter()
120 .map(|l| l.item.name.as_str())
121 .collect();
122 assert_eq!(added, vec!["fresh"]);
123 assert_eq!(removed, vec!["gone"]);
124 }
125
126 #[test]
127 fn reports_added_and_removed_references() {
128 let a = store(&[("lib.rs", "fn f() {}\nfn caller() { f(); }")]);
129 let b = store(&[("lib.rs", "fn f() {}\nfn g() {}\nfn caller() { g(); }")]);
131 let d = diff(&a, &b);
132
133 assert!(d.added_references.iter().any(|l| l.item.name == "g"));
134 assert!(d.removed_references.iter().any(|l| l.item.name == "f"));
135 assert!(d.added_symbols.iter().any(|l| l.item.name == "g"));
137 assert!(!d.removed_symbols.iter().any(|l| l.item.name == "f"));
138 }
139
140 #[test]
141 fn moving_a_symbol_across_files_is_add_plus_remove() {
142 let a = store(&[("a.rs", "fn moved() {}"), ("b.rs", "")]);
143 let b = store(&[("a.rs", ""), ("b.rs", "fn moved() {}")]);
144 let d = diff(&a, &b);
145 assert_eq!(d.added_symbols.len(), 1);
146 assert_eq!(d.added_symbols[0].path, "b.rs");
147 assert_eq!(d.removed_symbols.len(), 1);
148 assert_eq!(d.removed_symbols[0].path, "a.rs");
149 }
150
151 #[test]
152 fn line_shift_is_not_a_change() {
153 let a = store(&[("lib.rs", "fn a() {}")]);
154 let b = store(&[("lib.rs", "\n\nfn a() {}")]);
156 assert!(diff(&a, &b).is_empty());
157 }
158}