Skip to main content

fallow_extract/
source_map.rs

1//! Source-position mapping for code extracted from container formats.
2
3use oxc_span::Span;
4
5#[derive(Debug, Clone)]
6struct FragmentMap {
7    generated_start: u32,
8    original_start: u32,
9    len: u32,
10}
11
12/// Extracted source plus byte-offset mappings back to the original file.
13#[derive(Debug, Clone, Default)]
14pub struct ExtractionResult {
15    /// Source text passed to the JavaScript parser.
16    pub body: String,
17    fragments: Vec<FragmentMap>,
18}
19
20impl ExtractionResult {
21    /// Build a mapped result for one contiguous slice.
22    #[must_use]
23    pub fn contiguous(body: &str, original_start: usize) -> Self {
24        let mut result = Self::default();
25        result.push_mapped(body, original_start);
26        result
27    }
28
29    /// Append original source text to the extracted body.
30    pub fn push_mapped(&mut self, text: &str, original_start: usize) {
31        if text.is_empty() {
32            return;
33        }
34        let generated_start = self.body.len();
35        self.body.push_str(text);
36        self.fragments.push(FragmentMap {
37            generated_start: generated_start as u32,
38            original_start: original_start as u32,
39            len: text.len() as u32,
40        });
41    }
42
43    /// Map an extracted-buffer byte offset back to the original source, taking
44    /// the fragment that begins at `offset` when two fragments meet there.
45    fn original_offset_start_biased(&self, offset: u32) -> Option<u32> {
46        let idx = self
47            .fragments
48            .partition_point(|fragment| fragment.generated_start <= offset)
49            .checked_sub(1)?;
50        let fragment = &self.fragments[idx];
51        let delta = offset.checked_sub(fragment.generated_start)?;
52        if delta <= fragment.len {
53            Some(fragment.original_start + delta)
54        } else {
55            None
56        }
57    }
58
59    /// Map an extracted-buffer byte offset back to the original source, taking
60    /// the fragment that ends at `offset` when two fragments meet there.
61    fn original_offset_end_biased(&self, offset: u32) -> Option<u32> {
62        let idx = self
63            .fragments
64            .partition_point(|fragment| fragment.generated_start < offset)
65            .checked_sub(1)?;
66        let fragment = &self.fragments[idx];
67        let delta = offset.checked_sub(fragment.generated_start)?;
68        if delta <= fragment.len {
69            Some(fragment.original_start + delta)
70        } else {
71            None
72        }
73    }
74
75    /// Remap a span from extracted-buffer offsets to original-source offsets.
76    #[must_use]
77    pub fn remap_span(&self, span: Span) -> Span {
78        if span.start == 0 && span.end == 0 {
79            return span;
80        }
81        let Some(start) = self.original_offset_start_biased(span.start) else {
82            return span;
83        };
84        let Some(end) = self.original_offset_end_biased(span.end) else {
85            return span;
86        };
87        Span::new(start, end)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    /// Two adjacent fragments: `"ab"` from original offset 10, `"cd"` from 100.
96    /// Generated offset 2 is the seam between them.
97    fn two_fragments() -> ExtractionResult {
98        let mut result = ExtractionResult::default();
99        result.push_mapped("ab", 10);
100        result.push_mapped("cd", 100);
101        result
102    }
103
104    #[test]
105    fn start_biased_lookup_takes_the_fragment_beginning_at_the_offset() {
106        let result = two_fragments();
107        assert_eq!(result.original_offset_start_biased(0), Some(10));
108        assert_eq!(result.original_offset_start_biased(1), Some(11));
109        // The seam belongs to the second fragment for a span start.
110        assert_eq!(result.original_offset_start_biased(2), Some(100));
111    }
112
113    #[test]
114    fn end_biased_lookup_takes_the_fragment_ending_at_the_offset() {
115        let result = two_fragments();
116        // The same seam stays in the first fragment for a span end.
117        assert_eq!(result.original_offset_end_biased(2), Some(12));
118        assert_eq!(result.original_offset_end_biased(4), Some(102));
119        // Nothing precedes offset 0, so an end-biased lookup finds no fragment.
120        assert_eq!(result.original_offset_end_biased(0), None);
121    }
122
123    #[test]
124    fn a_fragment_end_maps_but_an_offset_past_it_does_not() {
125        let result = ExtractionResult::contiguous("abcde", 100);
126        assert_eq!(result.original_offset_start_biased(5), Some(105));
127        assert_eq!(result.original_offset_end_biased(5), Some(105));
128        assert_eq!(result.original_offset_start_biased(6), None);
129        assert_eq!(result.original_offset_end_biased(6), None);
130    }
131
132    #[test]
133    fn an_empty_result_maps_nothing() {
134        let result = ExtractionResult::default();
135        assert_eq!(result.original_offset_start_biased(0), None);
136        assert_eq!(result.original_offset_end_biased(0), None);
137    }
138
139    #[test]
140    fn remap_span_pairs_the_start_biased_and_end_biased_lookups() {
141        let result = two_fragments();
142        assert_eq!(result.remap_span(Span::new(1, 4)), Span::new(11, 102));
143        // A zero span is returned untouched.
144        assert_eq!(result.remap_span(Span::new(0, 0)), Span::new(0, 0));
145        // An unmappable endpoint leaves the whole span untouched.
146        assert_eq!(result.remap_span(Span::new(1, 9)), Span::new(1, 9));
147    }
148}