use std::ops::Range;
use std::sync::Arc;
use crate::source_info::SourceInfo;
use crate::types::FileId;
enum Root {
File(FileId),
Parent(Arc<SourceInfo>),
}
struct Piece {
src_range: Range<usize>,
content_len: usize,
verbatim: bool,
}
pub struct ProvenanceBuilder {
root: Root,
anchor: usize,
pieces: Vec<Piece>,
}
impl ProvenanceBuilder {
pub fn in_file(file_id: FileId, anchor: usize) -> Self {
Self {
root: Root::File(file_id),
anchor,
pieces: Vec::new(),
}
}
pub fn in_parent(parent: SourceInfo, anchor: usize) -> Self {
Self {
root: Root::Parent(Arc::new(parent)),
anchor,
pieces: Vec::new(),
}
}
pub fn verbatim(&mut self, src_range: Range<usize>) {
let content_len = src_range.len();
self.push(src_range, content_len, true);
}
pub fn replacement(&mut self, src_range: Range<usize>, out_len: usize) {
self.push(src_range, out_len, false);
}
fn push(&mut self, src_range: Range<usize>, content_len: usize, verbatim: bool) {
if verbatim
&& let Some(last) = self.pieces.last_mut()
&& last.verbatim
&& last.src_range.end == src_range.start
{
last.src_range.end = src_range.end;
last.content_len += content_len;
return;
}
self.pieces.push(Piece {
src_range,
content_len,
verbatim,
});
}
pub fn finish(self) -> SourceInfo {
#[cfg(debug_assertions)]
{
for pair in self.pieces.windows(2) {
debug_assert_eq!(
pair[0].src_range.end, pair[1].src_range.start,
"ProvenanceBuilder::finish: pieces do not tile their source \
contiguously (gap or overlap between adjacent pieces)"
);
}
}
if self.pieces.is_empty() {
return self.leaf(self.anchor, self.anchor);
}
if self.pieces.len() == 1 && self.pieces[0].verbatim {
let p = &self.pieces[0];
return self.leaf(p.src_range.start, p.src_range.end);
}
let concat_pieces: Vec<(SourceInfo, usize)> = self
.pieces
.iter()
.map(|p| (self.leaf(p.src_range.start, p.src_range.end), p.content_len))
.collect();
SourceInfo::concat(concat_pieces)
}
fn leaf(&self, start: usize, end: usize) -> SourceInfo {
match &self.root {
Root::File(file_id) => SourceInfo::Original {
file_id: *file_id,
start_offset: start,
end_offset: end,
},
Root::Parent(parent) => SourceInfo::Substring {
parent: Arc::clone(parent),
start_offset: start,
end_offset: end,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::FileId;
#[test]
fn test_all_verbatim_collapses_to_contiguous() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 100);
b.verbatim(0..3);
b.verbatim(3..7);
b.verbatim(7..10);
let si = b.finish();
match si {
SourceInfo::Original {
file_id,
start_offset,
end_offset,
} => {
assert_eq!(file_id, FileId(0));
assert_eq!(start_offset, 0);
assert_eq!(end_offset, 10);
}
other => panic!("expected a contiguous Original, got {other:?}"),
}
}
#[test]
fn test_fold_shape_stays_uncollapsed_concat() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 0);
b.verbatim(0..3);
b.replacement(3..4, 1);
b.verbatim(4..7);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 3, "fold shape must not collapse");
assert_eq!(si_length(&pieces[0].source_info), 3);
assert_eq!(pieces[0].length, 3);
assert_eq!(si_length(&pieces[1].source_info), 1);
assert_eq!(pieces[1].length, 1);
assert_eq!(si_length(&pieces[2].source_info), 3);
assert_eq!(pieces[2].length, 3);
}
other => panic!("expected a 3-piece Concat, got {other:?}"),
}
}
#[test]
fn test_zero_pieces_yields_zero_length_at_anchor() {
let b = ProvenanceBuilder::in_file(FileId(3), 42);
let si = b.finish();
match si {
SourceInfo::Original {
file_id,
start_offset,
end_offset,
} => {
assert_eq!(file_id, FileId(3));
assert_eq!(start_offset, 42);
assert_eq!(end_offset, 42);
}
other => panic!("expected a zero-length Original at the anchor, got {other:?}"),
}
}
#[test]
fn test_deletion_piece_is_stored_and_tiling_is_gap_free() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 0);
b.verbatim(4..7);
b.replacement(7..11, 0);
b.verbatim(11..14);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 3, "the deletion piece must not be dropped");
assert_eq!(si_length(&pieces[1].source_info), 4);
assert_eq!(pieces[1].length, 0, "deletion produces zero content bytes");
assert_eq!(source_range(&pieces[0].source_info), 4..7);
assert_eq!(source_range(&pieces[1].source_info), 7..11);
assert_eq!(source_range(&pieces[2].source_info), 11..14);
}
other => panic!("expected a 3-piece Concat, got {other:?}"),
}
}
#[test]
fn test_in_parent_over_concat_parent_yields_parent_relative_pieces() {
let parent = SourceInfo::concat(vec![
(SourceInfo::original(FileId(1), 0, 5), 5),
(SourceInfo::original(FileId(1), 10, 15), 5),
]);
assert!(
parent.resolve_byte_range().is_none(),
"sanity: Concat has no resolvable range"
);
let mut single = ProvenanceBuilder::in_parent(parent.clone(), 0);
single.verbatim(2..6);
match single.finish() {
SourceInfo::Substring {
parent: p,
start_offset,
end_offset,
} => {
assert!(matches!(*p, SourceInfo::Concat { .. }));
assert_eq!(start_offset, 2);
assert_eq!(end_offset, 6);
}
other => panic!("expected a Substring over the Concat parent, got {other:?}"),
}
let mut multi = ProvenanceBuilder::in_parent(parent.clone(), 0);
multi.verbatim(0..4);
multi.replacement(4..6, 1);
match multi.finish() {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 2);
for piece in &pieces {
match &piece.source_info {
SourceInfo::Substring { parent: p, .. } => {
assert!(matches!(**p, SourceInfo::Concat { .. }));
}
other => panic!("expected a Substring piece, got {other:?}"),
}
}
match &pieces[0].source_info {
SourceInfo::Substring {
start_offset,
end_offset,
..
} => {
assert_eq!(*start_offset, 0);
assert_eq!(*end_offset, 4);
}
_ => unreachable!(),
}
match &pieces[1].source_info {
SourceInfo::Substring {
start_offset,
end_offset,
..
} => {
assert_eq!(*start_offset, 4);
assert_eq!(*end_offset, 6);
}
_ => unreachable!(),
}
}
other => panic!("expected a 2-piece Concat, got {other:?}"),
}
}
#[test]
fn test_single_replacement_does_not_collapse() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 4);
b.replacement(4..6, 1);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 1);
assert_eq!(pieces[0].length, 1);
assert_eq!(si_length(&pieces[0].source_info), 2);
}
other => panic!("expected a 1-piece Concat, got {other:?}"),
}
}
#[test]
fn test_synthesis_empty_src_range_with_positive_out_len() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 7);
b.verbatim(7..10);
b.replacement(10..10, 1);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 2);
assert_eq!(pieces[1].length, 1);
assert_eq!(source_range(&pieces[1].source_info), 10..10);
}
other => panic!("expected a 2-piece Concat, got {other:?}"),
}
}
#[test]
fn test_adjacent_replacements_do_not_coalesce() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 0);
b.replacement(0..2, 1);
b.replacement(2..4, 1);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(
pieces.len(),
2,
"replacements never coalesce, even when adjacent"
);
assert_eq!(source_range(&pieces[0].source_info), 0..2);
assert_eq!(source_range(&pieces[1].source_info), 2..4);
}
other => panic!("expected a 2-piece Concat, got {other:?}"),
}
}
#[test]
fn test_replacement_at_offset_zero() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 0);
b.replacement(0..2, 1);
b.verbatim(2..5);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 2);
assert_eq!(source_range(&pieces[0].source_info), 0..2);
}
other => panic!("expected a 2-piece Concat, got {other:?}"),
}
}
#[test]
fn test_replacement_at_the_end() {
let mut b = ProvenanceBuilder::in_file(FileId(0), 0);
b.verbatim(0..3);
b.replacement(3..5, 1);
let si = b.finish();
match si {
SourceInfo::Concat { pieces } => {
assert_eq!(pieces.len(), 2);
assert_eq!(source_range(&pieces[1].source_info), 3..5);
assert_eq!(pieces[1].length, 1);
}
other => panic!("expected a 2-piece Concat, got {other:?}"),
}
}
fn si_length(si: &SourceInfo) -> usize {
si.length()
}
fn source_range(si: &SourceInfo) -> Range<usize> {
match si {
SourceInfo::Original {
start_offset,
end_offset,
..
} => *start_offset..*end_offset,
SourceInfo::Substring {
start_offset,
end_offset,
..
} => *start_offset..*end_offset,
other => panic!("source_range: unexpected variant {other:?}"),
}
}
}