Skip to main content

_diffctx/
core.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2
3use crate::types::{DiffHunk, Fragment, FragmentId, FragmentKind};
4
5fn kind_priority(kind: FragmentKind) -> u8 {
6    if kind.is_semantic() { 0 } else { 1 }
7}
8
9fn find_core_for_hunk(frags: &[&Fragment], h_start: u32, h_end: u32) -> FxHashSet<FragmentId> {
10    let mut core = FxHashSet::default();
11
12    let covering: Vec<&Fragment> = frags
13        .iter()
14        .copied()
15        .filter(|f| f.start_line() <= h_start && h_end <= f.end_line())
16        .collect();
17    // Every tie-break here ends on the fragment id. Kind class and span length
18    // leave genuine ties (nested or overlapping same-size definitions), and
19    // without a final total order the seed was decided by the order the parser
20    // emitted fragments in — so which code the output marks as changed shifted
21    // for reasons unrelated to the diff.
22    if !covering.is_empty() {
23        let best = covering
24            .iter()
25            .min_by(|a, b| {
26                let ka = kind_priority(a.kind);
27                let kb = kind_priority(b.kind);
28                ka.cmp(&kb)
29                    .then(a.line_count().cmp(&b.line_count()))
30                    .then_with(|| a.id.cmp(&b.id))
31            })
32            .unwrap();
33        core.insert(best.id.clone());
34        return core;
35    }
36
37    let overlapping: Vec<&Fragment> = frags
38        .iter()
39        .copied()
40        .filter(|f| f.start_line() <= h_end && f.end_line() >= h_start)
41        .collect();
42    if !overlapping.is_empty() {
43        for f in &overlapping {
44            core.insert(f.id.clone());
45        }
46        return core;
47    }
48
49    let before: Vec<&Fragment> = frags
50        .iter()
51        .copied()
52        .filter(|f| f.end_line() < h_start)
53        .collect();
54    let after: Vec<&Fragment> = frags
55        .iter()
56        .copied()
57        .filter(|f| f.start_line() > h_end)
58        .collect();
59    if let Some(nearest_before) = before.iter().max_by(|a, b| {
60        a.end_line()
61            .cmp(&b.end_line())
62            .then_with(|| a.id.cmp(&b.id))
63    }) {
64        core.insert(nearest_before.id.clone());
65    }
66    if let Some(nearest_after) = after.iter().min_by(|a, b| {
67        a.start_line()
68            .cmp(&b.start_line())
69            .then_with(|| a.id.cmp(&b.id))
70    }) {
71        core.insert(nearest_after.id.clone());
72    }
73
74    core
75}
76
77fn add_container_headers(
78    core_ids: &mut FxHashSet<FragmentId>,
79    frags_by_path: &FxHashMap<&str, Vec<&Fragment>>,
80) {
81    let core_paths: FxHashSet<&str> = core_ids.iter().map(|fid| fid.path.as_ref()).collect();
82    let mut headers_to_add = Vec::new();
83
84    for &path in &core_paths {
85        if let Some(frags) = frags_by_path.get(path) {
86            for frag in frags {
87                if !frag.kind.is_container() || core_ids.contains(&frag.id) {
88                    continue;
89                }
90                let contains_core = core_ids.iter().any(|core_id| {
91                    core_id.path.as_ref() == path
92                        && frag.start_line() <= core_id.start_line
93                        && core_id.end_line <= frag.end_line()
94                });
95                if contains_core {
96                    headers_to_add.push(frag.id.clone());
97                }
98            }
99        }
100    }
101
102    for h in headers_to_add {
103        core_ids.insert(h);
104    }
105}
106
107pub fn identify_core_fragments(
108    hunks: &[DiffHunk],
109    all_fragments: &[Fragment],
110) -> FxHashSet<FragmentId> {
111    let mut frags_by_path: FxHashMap<&str, Vec<&Fragment>> = FxHashMap::default();
112    for frag in all_fragments {
113        frags_by_path.entry(frag.path()).or_default().push(frag);
114    }
115
116    let mut core_ids = FxHashSet::default();
117    for h in hunks {
118        if let Some(frags) = frags_by_path.get(h.path.as_ref()) {
119            let (h_start, h_end) = h.core_selection_range();
120            core_ids.extend(find_core_for_hunk(frags, h_start, h_end));
121        }
122    }
123
124    add_container_headers(&mut core_ids, &frags_by_path);
125    core_ids
126}
127
128fn map_hunks_to_fragments(
129    hunks: &[DiffHunk],
130    core_ids: &FxHashSet<FragmentId>,
131    all_fragments: &[Fragment],
132) -> FxHashMap<FragmentId, f64> {
133    let mut result: FxHashMap<FragmentId, f64> = FxHashMap::default();
134    for h in hunks {
135        let (h_start, h_end) = h.core_selection_range();
136        let hunk_size = (h_end as i64 - h_start as i64 + 1).max(1) as f64;
137        for frag in all_fragments {
138            if !core_ids.contains(&frag.id) || frag.path() != h.path.as_ref() {
139                continue;
140            }
141            if frag.start_line() <= h_end && frag.end_line() >= h_start {
142                *result.entry(frag.id.clone()).or_insert(0.0) += hunk_size;
143            }
144        }
145    }
146    result
147}
148
149fn add_container_weights(
150    frag_hunk_lines: &mut FxHashMap<FragmentId, f64>,
151    core_ids: &FxHashSet<FragmentId>,
152    all_fragments: &[Fragment],
153) {
154    let mut to_add = Vec::new();
155    for frag in all_fragments {
156        if !core_ids.contains(&frag.id) || frag_hunk_lines.contains_key(&frag.id) {
157            continue;
158        }
159        if !frag.kind.is_container() {
160            continue;
161        }
162        let contained_weight: f64 = frag_hunk_lines
163            .iter()
164            .filter(|(fid, _)| {
165                fid.path.as_ref() == frag.path()
166                    && frag.start_line() <= fid.start_line
167                    && fid.end_line <= frag.end_line()
168            })
169            .map(|(_, w)| *w)
170            .sum();
171        if contained_weight > 0.0 {
172            to_add.push((frag.id.clone(), contained_weight));
173        }
174    }
175    for (id, w) in to_add {
176        frag_hunk_lines.insert(id, w);
177    }
178}
179
180fn best_hunk_size_for_path(hunks: &[DiffHunk], path: &str) -> u32 {
181    let mut best = 0u32;
182    for h in hunks {
183        if h.path.as_ref() == path {
184            let (h_start, h_end) = h.core_selection_range();
185            let size = h_end.saturating_sub(h_start) + 1;
186            best = best.max(size);
187        }
188    }
189    best
190}
191
192fn fill_missing_core_weights(
193    frag_hunk_lines: &mut FxHashMap<FragmentId, f64>,
194    core_ids: &FxHashSet<FragmentId>,
195    hunks: &[DiffHunk],
196) {
197    let missing: Vec<FragmentId> = core_ids
198        .iter()
199        .filter(|fid| !frag_hunk_lines.contains_key(*fid))
200        .cloned()
201        .collect();
202    for fid in missing {
203        let best = best_hunk_size_for_path(hunks, fid.path.as_ref());
204        if best > 0 {
205            frag_hunk_lines.insert(fid, best as f64);
206        }
207    }
208}
209
210pub fn compute_seed_weights(
211    hunks: &[DiffHunk],
212    core_ids: &FxHashSet<FragmentId>,
213    all_fragments: &[Fragment],
214) -> FxHashMap<FragmentId, f64> {
215    let mut frag_hunk_lines = map_hunks_to_fragments(hunks, core_ids, all_fragments);
216    if frag_hunk_lines.is_empty() {
217        return FxHashMap::default();
218    }
219
220    add_container_weights(&mut frag_hunk_lines, core_ids, all_fragments);
221    fill_missing_core_weights(&mut frag_hunk_lines, core_ids, hunks);
222
223    frag_hunk_lines
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use std::sync::Arc;
230
231    fn frag(path: &str, start: u32, end: u32, kind: FragmentKind) -> Fragment {
232        Fragment {
233            id: FragmentId::new(Arc::from(path), start, end),
234            kind,
235            content: Arc::from(""),
236            identifiers: FxHashSet::default(),
237            token_count: 10,
238            symbol_name: None,
239        }
240    }
241
242    fn hunk(path: &str, start: u32, len: u32) -> DiffHunk {
243        DiffHunk {
244            path: Arc::from(path),
245            new_start: start,
246            new_len: len,
247            old_start: start,
248            old_len: len,
249        }
250    }
251
252    fn sorted_ids(core: &FxHashSet<FragmentId>) -> Vec<FragmentId> {
253        let mut v: Vec<FragmentId> = core.iter().cloned().collect();
254        v.sort();
255        v
256    }
257
258    /// Which fragment a hunk seeds decides what the output marks as changed.
259    /// The tie-breaks in `find_core_for_hunk` compare kind class and span
260    /// length only, so equally-ranked candidates were resolved by whatever
261    /// order the parser happened to emit them in — the same order dependence
262    /// already ruled out for `drop_redundant_signatures` and
263    /// `cap_context_fragments`.
264    #[test]
265    fn core_identification_is_invariant_under_fragment_order() {
266        // Two equally-sized covering candidates, plus an equally-distant
267        // neighbour on each side of a second hunk that nothing covers.
268        let fragments = vec![
269            frag("a.rs", 10, 20, FragmentKind::Function),
270            frag("a.rs", 15, 25, FragmentKind::Function),
271            frag("a.rs", 60, 70, FragmentKind::Function),
272            frag("a.rs", 90, 100, FragmentKind::Function),
273            frag("b.rs", 1, 11, FragmentKind::Function),
274            frag("b.rs", 30, 40, FragmentKind::Function),
275        ];
276        let hunks = vec![
277            hunk("a.rs", 16, 3),
278            hunk("a.rs", 80, 1),
279            hunk("b.rs", 20, 1),
280        ];
281
282        let baseline = sorted_ids(&identify_core_fragments(&hunks, &fragments));
283        assert!(!baseline.is_empty(), "no core identified at all");
284
285        for permuted in [
286            {
287                let mut v = fragments.clone();
288                v.reverse();
289                v
290            },
291            {
292                let mut v = fragments.clone();
293                v.rotate_left(3);
294                v
295            },
296            {
297                let mut v = fragments.clone();
298                v.sort_by_key(|f| std::cmp::Reverse(f.start_line()));
299                v
300            },
301        ] {
302            assert_eq!(
303                sorted_ids(&identify_core_fragments(&hunks, &permuted)),
304                baseline,
305                "core set changed with fragment order"
306            );
307        }
308    }
309}