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