Skip to main content

diffkit/
diff.rs

1use crate::model::{CallLabel, CallNode, CallRelation, CallSiteId};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub enum DiffStatus {
5    Same,
6    Added,
7    Removed,
8    Modified,
9}
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct DiffNode {
13    pub key: String,
14    pub callsite: Option<CallSiteId>,
15    pub label: CallLabel,
16    pub relation: CallRelation,
17    pub before_label: Option<CallLabel>,
18    pub before_relation: Option<CallRelation>,
19    pub status: DiffStatus,
20    pub children: Vec<DiffNode>,
21}
22
23pub fn diff_optional(before: Option<&CallNode>, after: Option<&CallNode>) -> Option<DiffNode> {
24    match (before, after) {
25        (Some(before), Some(after)) => Some(diff_node(Some(before), Some(after))),
26        (Some(before), None) => Some(diff_node(Some(before), None)),
27        (None, Some(after)) => Some(diff_node(None, Some(after))),
28        (None, None) => None,
29    }
30}
31
32fn diff_node(before: Option<&CallNode>, after: Option<&CallNode>) -> DiffNode {
33    match (before, after) {
34        (Some(before), Some(after)) => DiffNode {
35            key: after.key.clone(),
36            callsite: after.callsite.clone(),
37            label: after.label.clone(),
38            relation: after.relation,
39            before_label: (!labels_equivalent(&before.label.default, &after.label.default)
40                || before.relation != after.relation)
41                .then(|| before.label.clone()),
42            before_relation: (before.relation != after.relation).then_some(before.relation),
43            status: if labels_equivalent(&before.label.default, &after.label.default)
44                && before.relation == after.relation
45            {
46                DiffStatus::Same
47            } else {
48                DiffStatus::Modified
49            },
50            children: diff_children(&before.children, &after.children),
51        },
52        (None, Some(after)) => mark_tree(after, DiffStatus::Added),
53        (Some(before), None) => mark_tree(before, DiffStatus::Removed),
54        (None, None) => unreachable!("diff_node requires at least one node"),
55    }
56}
57
58fn mark_tree(node: &CallNode, status: DiffStatus) -> DiffNode {
59    DiffNode {
60        key: node.key.clone(),
61        callsite: node.callsite.clone(),
62        label: node.label.clone(),
63        relation: node.relation,
64        before_label: None,
65        before_relation: None,
66        status,
67        children: node
68            .children
69            .iter()
70            .map(|child| mark_tree(child, status))
71            .collect(),
72    }
73}
74
75fn diff_children(before: &[CallNode], after: &[CallNode]) -> Vec<DiffNode> {
76    let n = before.len();
77    let m = after.len();
78    let mut score = vec![vec![0usize; m + 1]; n + 1];
79
80    for i in (0..n).rev() {
81        for j in (0..m).rev() {
82            let matched = alignment_weight(&before[i], &after[j])
83                .map(|weight| score[i + 1][j + 1] + weight)
84                .unwrap_or_default();
85            score[i][j] = matched.max(score[i + 1][j]).max(score[i][j + 1]);
86        }
87    }
88
89    let mut result = Vec::new();
90    let (mut i, mut j) = (0, 0);
91    while i < n && j < m {
92        let matched =
93            alignment_weight(&before[i], &after[j]).map(|weight| score[i + 1][j + 1] + weight);
94        if matched == Some(score[i][j]) {
95            result.push(diff_node(Some(&before[i]), Some(&after[j])));
96            i += 1;
97            j += 1;
98        } else if score[i + 1][j] >= score[i][j + 1] {
99            result.push(diff_node(Some(&before[i]), None));
100            i += 1;
101        } else {
102            result.push(diff_node(None, Some(&after[j])));
103            j += 1;
104        }
105    }
106    while i < n {
107        result.push(diff_node(Some(&before[i]), None));
108        i += 1;
109    }
110    while j < m {
111        result.push(diff_node(None, Some(&after[j])));
112        j += 1;
113    }
114    result
115}
116
117/// Prefer stable call-site identity and unchanged source labels while still
118/// allowing a modified call to align by semantic target. A plain key-only LCS
119/// pairs the wrong occurrences when the same callee is called repeatedly and
120/// one occurrence is inserted or removed.
121fn alignment_weight(before: &CallNode, after: &CallNode) -> Option<usize> {
122    if before.key != after.key {
123        return None;
124    }
125    let mut weight = 10usize;
126    if before.callsite.is_some() && before.callsite == after.callsite {
127        weight += 1_000;
128    }
129    if before.label.default == after.label.default {
130        weight += 100;
131    } else if labels_equivalent(&before.label.default, &after.label.default) {
132        weight += 80;
133    }
134    if before.relation == after.relation {
135        weight += 5;
136    }
137    if same_shallow_children(before, after) {
138        weight += 25;
139    }
140    Some(weight)
141}
142
143fn labels_equivalent(before: &str, after: &str) -> bool {
144    before == after || normalize_anonymous_lambdas(before) == normalize_anonymous_lambdas(after)
145}
146
147fn normalize_anonymous_lambdas(label: &str) -> String {
148    let mut normalized = String::with_capacity(label.len());
149    let mut rest = label;
150    while let Some(index) = rest.find("λ#") {
151        normalized.push_str(&rest[..index + "λ#".len()]);
152        rest = &rest[index + "λ#".len()..];
153        let digits = rest.chars().take_while(char::is_ascii_digit).count();
154        rest = &rest[digits..];
155    }
156    normalized.push_str(rest);
157    normalized
158}
159
160fn same_shallow_children(before: &CallNode, after: &CallNode) -> bool {
161    before.children.len() == after.children.len()
162        && before
163            .children
164            .iter()
165            .zip(&after.children)
166            .all(|(before, after)| {
167                before.key == after.key && before.label.default == after.label.default
168            })
169}
170
171/// Apply the presentation depth after the complete trees have been compared.
172/// A hidden changed path is represented explicitly instead of being mistaken
173/// for "no call changes".
174pub fn truncate_diff_tree(node: &mut DiffNode, max_depth: usize) {
175    truncate_diff_tree_at(node, max_depth, 0);
176}
177
178fn truncate_diff_tree_at(node: &mut DiffNode, max_depth: usize, depth: usize) {
179    if depth >= max_depth {
180        if node.children.iter().any(tree_has_changes) {
181            node.children = vec![DiffNode {
182                key: format!("{}#depth-limit", node.key),
183                callsite: None,
184                label: CallLabel::new("… changed below max depth"),
185                relation: CallRelation::Call,
186                before_label: None,
187                before_relation: None,
188                status: DiffStatus::Added,
189                children: Vec::new(),
190            }];
191        } else {
192            node.children.clear();
193        }
194        return;
195    }
196    for child in &mut node.children {
197        truncate_diff_tree_at(child, max_depth, depth + 1);
198    }
199}
200
201pub fn tree_has_changes(node: &DiffNode) -> bool {
202    node.status != DiffStatus::Same || node.children.iter().any(tree_has_changes)
203}
204
205/// Keep unchanged siblings as one-line context while hiding their unchanged
206/// descendants. Paths that lead to a change and fully added/removed subtrees
207/// remain expanded.
208pub fn collapse_unchanged_subtrees(node: &mut DiffNode) -> bool {
209    let carries_dispatch_context = subtree_has_dispatch_context(node);
210    let mut descendants_changed = false;
211    for child in &mut node.children {
212        descendants_changed |= collapse_unchanged_subtrees(child);
213    }
214    let changed = node.status != DiffStatus::Same || descendants_changed;
215    if !changed && !carries_dispatch_context {
216        node.children.clear();
217    }
218    changed
219}
220
221fn subtree_has_dispatch_context(node: &DiffNode) -> bool {
222    node.relation == CallRelation::DispatchCandidate
223        || node.children.iter().any(subtree_has_dispatch_context)
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::model::{CallLabel, CallSiteId};
230
231    fn node(key: &str, label: CallLabel, children: Vec<CallNode>) -> CallNode {
232        CallNode {
233            key: key.to_owned(),
234            callsite: None,
235            label,
236            relation: CallRelation::Call,
237            children,
238        }
239    }
240
241    fn call(key: &str, site: &str, label: &str) -> CallNode {
242        CallNode {
243            key: key.to_owned(),
244            callsite: Some(CallSiteId(site.to_owned())),
245            label: CallLabel::new(label),
246            relation: CallRelation::Call,
247            children: Vec::new(),
248        }
249    }
250
251    #[test]
252    fn aligns_by_semantic_key_and_records_a_display_change() {
253        let before = node("rust://save", CallLabel::new("save(order)"), Vec::new());
254        let after = node(
255            "rust://save",
256            CallLabel::new("save(next_order)"),
257            Vec::new(),
258        );
259
260        let diff = diff_optional(Some(&before), Some(&after)).unwrap();
261        assert_eq!(diff.key, "rust://save");
262        assert_eq!(diff.status, DiffStatus::Modified);
263        assert_eq!(
264            diff.before_label
265                .as_ref()
266                .map(|label| label.default.as_str()),
267            Some("save(order)")
268        );
269    }
270
271    #[test]
272    fn typed_labels_do_not_create_noise_in_the_default_call_diff() {
273        let before = node(
274            "rust://save",
275            CallLabel::with_types("save(order)", "save(order: OldOrder)"),
276            Vec::new(),
277        );
278        let after = node(
279            "rust://save",
280            CallLabel::with_types("save(order)", "save(order: NewOrder)"),
281            Vec::new(),
282        );
283
284        let diff = diff_optional(Some(&before), Some(&after)).unwrap();
285        assert_eq!(diff.status, DiffStatus::Same);
286        assert!(!tree_has_changes(&diff));
287    }
288
289    #[test]
290    fn collapses_unchanged_context_below_its_first_line() {
291        let stable = node(
292            "rust://stable",
293            CallLabel::new("stable()"),
294            vec![node("rust://deep", CallLabel::new("deep()"), Vec::new())],
295        );
296        let before = node(
297            "rust://root",
298            CallLabel::new("root()"),
299            vec![
300                stable.clone(),
301                node("rust://old", CallLabel::new("old()"), Vec::new()),
302            ],
303        );
304        let after = node(
305            "rust://root",
306            CallLabel::new("root()"),
307            vec![
308                stable,
309                node("rust://new", CallLabel::new("new()"), Vec::new()),
310            ],
311        );
312        let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
313
314        assert!(collapse_unchanged_subtrees(&mut diff));
315        assert_eq!(diff.children[0].label.default, "stable()");
316        assert!(diff.children[0].children.is_empty());
317    }
318
319    #[test]
320    fn aligns_repeated_callees_by_callsite_and_label_before_target_fallback() {
321        let before = node(
322            "rust://root",
323            CallLabel::new("root()"),
324            vec![
325                call("rust://save", "save@1", "save(first)"),
326                call("rust://save", "save@2", "save(second)"),
327            ],
328        );
329        let after = node(
330            "rust://root",
331            CallLabel::new("root()"),
332            vec![
333                call("rust://save", "save@0", "save(inserted)"),
334                call("rust://save", "save@1", "save(first)"),
335                call("rust://save", "save@2", "save(second)"),
336            ],
337        );
338
339        let diff = diff_optional(Some(&before), Some(&after)).unwrap();
340        assert_eq!(
341            diff.children
342                .iter()
343                .map(|node| (node.label.default.as_str(), node.status))
344                .collect::<Vec<_>>(),
345            vec![
346                ("save(inserted)", DiffStatus::Added),
347                ("save(first)", DiffStatus::Same),
348                ("save(second)", DiffStatus::Same),
349            ]
350        );
351    }
352
353    #[test]
354    fn preserves_a_deep_change_when_presentation_depth_is_small() {
355        let before = node(
356            "root",
357            CallLabel::new("root()"),
358            vec![node(
359                "a",
360                CallLabel::new("a()"),
361                vec![node("b", CallLabel::new("b()"), Vec::new())],
362            )],
363        );
364        let after = node(
365            "root",
366            CallLabel::new("root()"),
367            vec![node(
368                "a",
369                CallLabel::new("a()"),
370                vec![node(
371                    "b",
372                    CallLabel::new("b()"),
373                    vec![node("new", CallLabel::new("new()"), Vec::new())],
374                )],
375            )],
376        );
377        let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
378        assert!(tree_has_changes(&diff));
379        truncate_diff_tree(&mut diff, 1);
380        assert_eq!(
381            diff.children[0].children[0].label.default,
382            "… changed below max depth"
383        );
384    }
385}