Skip to main content

oak_source_map/
decoder.rs

1//! Source Map Decoder for efficient lookup.
2
3use std::collections::BTreeMap;
4
5use crate::{BoundedMapping, Mapping, Result, SourceMap, SourceMapError};
6
7/// Decoder for efficient source map lookups.
8///
9/// This provides O(log n) lookup for original positions from generated positions.
10#[derive(Debug, Clone)]
11pub struct SourceMapDecoder {
12    source_map: SourceMap,
13    lines: BTreeMap<u32, Vec<BoundedMapping>>,
14    sources: Vec<String>,
15    names: Vec<String>,
16}
17
18impl SourceMapDecoder {
19    /// Creates a new decoder from a source map.
20    pub fn new(source_map: SourceMap) -> Result<Self> {
21        let mappings = source_map.parse_mappings()?;
22        let mut lines: BTreeMap<u32, Vec<BoundedMapping>> = BTreeMap::new();
23
24        for mapping in mappings {
25            let line = mapping.generated_line;
26            let col = mapping.generated_column;
27
28            let line_mappings = lines.entry(line).or_default();
29
30            if let Some(last) = line_mappings.last_mut() {
31                last.end_column = col;
32            }
33
34            line_mappings.push(BoundedMapping::new(mapping, col, u32::MAX));
35        }
36
37        Ok(Self { source_map, lines, sources: Vec::new(), names: Vec::new() })
38    }
39
40    /// Looks up the original position for a generated position.
41    pub fn lookup(&self, generated_line: u32, generated_column: u32) -> Option<&Mapping> {
42        let line_mappings = self.lines.get(&generated_line)?;
43
44        let idx = line_mappings
45            .binary_search_by(|m| {
46                if m.end_column <= generated_column {
47                    std::cmp::Ordering::Less
48                }
49                else if m.start_column > generated_column {
50                    std::cmp::Ordering::Greater
51                }
52                else {
53                    std::cmp::Ordering::Equal
54                }
55            })
56            .ok()?;
57
58        Some(&line_mappings[idx].mapping)
59    }
60
61    /// Looks up the original position and returns full information.
62    pub fn lookup_full(&self, generated_line: u32, generated_column: u32) -> Option<OriginalPosition> {
63        let mapping = self.lookup(generated_line, generated_column)?;
64
65        let source = mapping.source_index.and_then(|idx| self.source_map.get_source(idx as usize));
66
67        let name = mapping.name_index.and_then(|idx| self.source_map.get_name(idx as usize));
68
69        Some(OriginalPosition { source: source.map(String::from), original_line: mapping.original_line, original_column: mapping.original_column, name: name.map(String::from) })
70    }
71
72    /// Returns all mappings for a generated line.
73    pub fn get_line_mappings(&self, line: u32) -> Option<&[BoundedMapping]> {
74        self.lines.get(&line).map(|v| v.as_slice())
75    }
76
77    /// Returns the number of lines in the generated file.
78    pub fn generated_line_count(&self) -> usize {
79        self.lines.len()
80    }
81
82    /// Returns the underlying source map.
83    pub fn source_map(&self) -> &SourceMap {
84        &self.source_map
85    }
86
87    /// Iterates over all mappings.
88    pub fn iter_mappings(&self) -> impl Iterator<Item = &BoundedMapping> {
89        self.lines.values().flat_map(|v| v.iter())
90    }
91}
92
93/// Original position information from a lookup.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct OriginalPosition {
96    /// Source file path.
97    pub source: Option<String>,
98    /// Original line (0-indexed).
99    pub original_line: Option<u32>,
100    /// Original column (0-indexed).
101    pub original_column: Option<u32>,
102    /// Symbol name.
103    pub name: Option<String>,
104}
105
106impl OriginalPosition {
107    /// Creates a new original position.
108    pub fn new(source: Option<String>, original_line: Option<u32>, original_column: Option<u32>, name: Option<String>) -> Self {
109        Self { source, original_line, original_column, name }
110    }
111
112    /// Checks if this position has source information.
113    pub fn has_source(&self) -> bool {
114        self.source.is_some()
115    }
116
117    /// Checks if this position has a name.
118    pub fn has_name(&self) -> bool {
119        self.name.is_some()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_decoder_lookup() {
129        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,SAASA"}"#;
130        let sm = SourceMap::parse(json).unwrap();
131        let decoder = SourceMapDecoder::new(sm).unwrap();
132
133        let pos = decoder.lookup(0, 0);
134        assert!(pos.is_some());
135    }
136
137    #[test]
138    fn test_decoder_lookup_full() {
139        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAA,SAASA"}"#;
140        let sm = SourceMap::parse(json).unwrap();
141        let decoder = SourceMapDecoder::new(sm).unwrap();
142
143        let pos = decoder.lookup_full(0, 0);
144        assert!(pos.is_some());
145
146        let pos = pos.unwrap();
147        assert_eq!(pos.source, Some("a.js".to_string()));
148    }
149}