1use std::sync::Arc;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::types::{Fragment, FragmentId};
6
7pub struct IntervalIndex {
8 by_path: FxHashMap<Arc<str>, Vec<(u32, u32)>>,
9 ids: FxHashSet<FragmentId>,
10}
11
12impl IntervalIndex {
13 pub fn new() -> Self {
14 Self {
15 by_path: FxHashMap::default(),
16 ids: FxHashSet::default(),
17 }
18 }
19
20 pub fn add(&mut self, frag: &Fragment) {
21 self.add_id(&frag.id);
22 }
23
24 pub fn add_id(&mut self, frag_id: &FragmentId) {
25 self.ids.insert(frag_id.clone());
26 let intervals = self.by_path.entry(frag_id.path.clone()).or_default();
27 let item = (frag_id.start_line, frag_id.end_line);
28 let pos = intervals.binary_search(&item).unwrap_or_else(|e| e);
29 intervals.insert(pos, item);
30 }
31
32 pub fn contains(&self, frag_id: &FragmentId) -> bool {
33 self.ids.contains(frag_id)
34 }
35
36 pub fn overlaps(&self, frag: &Fragment) -> bool {
37 let intervals = match self.by_path.get(&frag.id.path) {
38 Some(v) => v,
39 None => return false,
40 };
41 let upper = intervals.partition_point(|&(s, _)| s <= frag.end_line());
42 for i in 0..upper {
43 let (start, end) = intervals[i];
44 if start == frag.start_line() && end == frag.end_line() {
45 continue;
46 }
47 if end > frag.start_line() {
69 return true;
70 }
71 }
72 false
73 }
74
75 pub fn is_superset_of(&self, frag: &Fragment) -> bool {
76 let intervals = match self.by_path.get(&frag.id.path) {
77 Some(v) => v,
78 None => return false,
79 };
80 let upper = intervals.partition_point(|&(s, _)| s <= frag.start_line());
81 for i in 0..upper {
82 let (start, end) = intervals[i];
83 if start == frag.start_line() && end == frag.end_line() {
84 continue;
85 }
86 if start <= frag.start_line() && frag.end_line() <= end {
87 return true;
88 }
89 }
90 false
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::types::FragmentKind;
98
99 fn frag(path: &str, start: u32, end: u32) -> Fragment {
100 Fragment {
101 id: FragmentId::new(Arc::from(path), start, end),
102 kind: FragmentKind::Function,
103 content: Arc::from(""),
104 identifiers: FxHashSet::default(),
105 token_count: 1,
106 symbol_name: None,
107 }
108 }
109
110 fn index_with(spans: &[(u32, u32)]) -> IntervalIndex {
111 let mut idx = IntervalIndex::new();
112 for &(s, e) in spans {
113 idx.add(&frag("a.rs", s, e));
114 }
115 idx
116 }
117
118 #[test]
119 fn overlaps_is_true_for_genuine_intersections() {
120 let idx = index_with(&[(10, 20)]);
121 for &(s, e) in &[(15, 25), (11, 19), (9, 21), (5, 15), (10, 20 + 1)] {
122 assert!(
123 idx.overlaps(&frag("a.rs", s, e)),
124 "[{s},{e}] should intersect [10,20]"
125 );
126 }
127 }
128
129 #[test]
136 fn overlaps_tolerates_a_shared_boundary_in_one_direction_only() {
137 assert!(
138 !index_with(&[(1, 10)]).overlaps(&frag("a.rs", 10, 20)),
139 "candidate starting on the selected fragment's last line must be tolerated"
140 );
141 assert!(
142 index_with(&[(10, 20)]).overlaps(&frag("a.rs", 1, 10)),
143 "the mirrored case is currently reported as overlapping; if this now \
144 passes as tolerated, the symmetry fix landed — update the corpus baseline"
145 );
146 }
147
148 #[test]
149 fn overlaps_verdicts_are_pinned_across_the_boundary_matrix() {
150 let expected = [
154 ((1, 10), (10, 20), false),
155 ((10, 20), (1, 10), true),
156 ((10, 20), (20, 30), false),
157 ((20, 30), (10, 20), true),
158 ((10, 20), (11, 19), true),
159 ((11, 19), (10, 20), true),
160 ((10, 20), (9, 21), true),
161 ((9, 21), (10, 20), true),
162 ((10, 20), (10, 10), true),
165 ((10, 10), (10, 20), false),
166 ];
167 for (selected, candidate, want) in expected {
168 let got = index_with(&[selected]).overlaps(&frag("a.rs", candidate.0, candidate.1));
169 assert_eq!(
170 got, want,
171 "selected {selected:?} vs candidate {candidate:?}: got {got}, want {want}"
172 );
173 }
174 }
175
176 #[test]
177 fn overlaps_ignores_other_paths() {
178 let idx = index_with(&[(10, 20)]);
179 assert!(!idx.overlaps(&frag("b.rs", 15, 16)));
180 }
181
182 #[test]
183 fn identical_span_is_reported_by_contains_not_by_overlaps() {
184 let idx = index_with(&[(10, 20)]);
187 let same = frag("a.rs", 10, 20);
188 assert!(!idx.overlaps(&same));
189 assert!(idx.contains(&same.id));
190 }
191
192 #[test]
193 fn is_superset_of_detects_enclosure_and_ignores_identical_spans() {
194 let idx = index_with(&[(10, 30)]);
195 assert!(idx.is_superset_of(&frag("a.rs", 15, 25)));
196 assert!(idx.is_superset_of(&frag("a.rs", 10, 25)));
197 assert!(!idx.is_superset_of(&frag("a.rs", 10, 30)));
198 assert!(!idx.is_superset_of(&frag("a.rs", 5, 25)));
199 assert!(!idx.is_superset_of(&frag("a.rs", 25, 35)));
200 }
201
202 #[test]
203 fn add_keeps_intervals_sorted_regardless_of_insertion_order() {
204 let forward = index_with(&[(1, 5), (10, 20), (30, 40)]);
205 let shuffled = index_with(&[(30, 40), (1, 5), (10, 20)]);
206 let path: Arc<str> = Arc::from("a.rs");
207 assert_eq!(forward.by_path[&path], shuffled.by_path[&path]);
208 assert!(forward.by_path[&path].windows(2).all(|w| w[0] <= w[1]));
209 }
210}