Skip to main content

oxihuman_core/
source_map.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Source map: maps generated byte offsets to original (file, line, col) positions.
6
7/// A single source mapping entry.
8#[allow(dead_code)]
9#[derive(Debug, Clone, PartialEq)]
10pub struct SourceMapping {
11    pub generated_offset: u32,
12    pub source_file: String,
13    pub source_line: u32,
14    pub source_col: u32,
15    pub name: Option<String>,
16}
17
18/// Source map holding a list of mappings sorted by generated offset.
19#[allow(dead_code)]
20pub struct SourceMap {
21    mappings: Vec<SourceMapping>,
22    source_files: Vec<String>,
23}
24
25#[allow(dead_code)]
26impl SourceMap {
27    pub fn new() -> Self {
28        Self {
29            mappings: Vec::new(),
30            source_files: Vec::new(),
31        }
32    }
33
34    /// Add a mapping entry.
35    pub fn add_mapping(
36        &mut self,
37        generated_offset: u32,
38        source_file: &str,
39        source_line: u32,
40        source_col: u32,
41        name: Option<&str>,
42    ) {
43        if !self.source_files.contains(&source_file.to_string()) {
44            self.source_files.push(source_file.to_string());
45        }
46        self.mappings.push(SourceMapping {
47            generated_offset,
48            source_file: source_file.to_string(),
49            source_line,
50            source_col,
51            name: name.map(|s| s.to_string()),
52        });
53        self.mappings.sort_unstable_by_key(|m| m.generated_offset);
54    }
55
56    /// Look up the source position for a generated offset.
57    pub fn lookup(&self, generated_offset: u32) -> Option<&SourceMapping> {
58        if self.mappings.is_empty() {
59            return None;
60        }
61        match self
62            .mappings
63            .binary_search_by_key(&generated_offset, |m| m.generated_offset)
64        {
65            Ok(i) => Some(&self.mappings[i]),
66            Err(i) => {
67                if i == 0 {
68                    None
69                } else {
70                    Some(&self.mappings[i - 1])
71                }
72            }
73        }
74    }
75
76    pub fn mapping_count(&self) -> usize {
77        self.mappings.len()
78    }
79
80    pub fn source_file_count(&self) -> usize {
81        self.source_files.len()
82    }
83
84    pub fn source_files(&self) -> &[String] {
85        &self.source_files
86    }
87
88    pub fn is_empty(&self) -> bool {
89        self.mappings.is_empty()
90    }
91
92    /// All mappings for a specific source file.
93    pub fn mappings_for_file(&self, file: &str) -> Vec<&SourceMapping> {
94        self.mappings
95            .iter()
96            .filter(|m| m.source_file == file)
97            .collect()
98    }
99
100    pub fn clear(&mut self) {
101        self.mappings.clear();
102        self.source_files.clear();
103    }
104}
105
106impl Default for SourceMap {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112pub fn new_source_map() -> SourceMap {
113    SourceMap::new()
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn add_and_lookup_exact() {
122        let mut sm = new_source_map();
123        sm.add_mapping(100, "main.rs", 10, 5, None);
124        let m = sm.lookup(100).expect("should succeed");
125        assert_eq!(m.source_line, 10);
126    }
127
128    #[test]
129    fn lookup_between_entries() {
130        let mut sm = new_source_map();
131        sm.add_mapping(0, "a.rs", 1, 0, None);
132        sm.add_mapping(50, "a.rs", 5, 0, None);
133        let m = sm.lookup(25).expect("should succeed");
134        assert_eq!(m.source_line, 1);
135    }
136
137    #[test]
138    fn lookup_before_first_returns_none() {
139        let mut sm = new_source_map();
140        sm.add_mapping(10, "a.rs", 1, 0, None);
141        assert!(sm.lookup(0).is_none());
142    }
143
144    #[test]
145    fn source_file_deduplication() {
146        let mut sm = new_source_map();
147        sm.add_mapping(0, "a.rs", 1, 0, None);
148        sm.add_mapping(1, "a.rs", 2, 0, None);
149        assert_eq!(sm.source_file_count(), 1);
150    }
151
152    #[test]
153    fn multiple_source_files() {
154        let mut sm = new_source_map();
155        sm.add_mapping(0, "a.rs", 1, 0, None);
156        sm.add_mapping(10, "b.rs", 1, 0, None);
157        assert_eq!(sm.source_file_count(), 2);
158    }
159
160    #[test]
161    fn mappings_for_file() {
162        let mut sm = new_source_map();
163        sm.add_mapping(0, "a.rs", 1, 0, None);
164        sm.add_mapping(10, "b.rs", 1, 0, None);
165        assert_eq!(sm.mappings_for_file("a.rs").len(), 1);
166    }
167
168    #[test]
169    fn named_mapping() {
170        let mut sm = new_source_map();
171        sm.add_mapping(0, "a.rs", 1, 0, Some("main"));
172        let m = sm.lookup(0).expect("should succeed");
173        assert_eq!(m.name.as_deref(), Some("main"));
174    }
175
176    #[test]
177    fn empty_map_is_empty() {
178        let sm = new_source_map();
179        assert!(sm.is_empty());
180    }
181
182    #[test]
183    fn clear() {
184        let mut sm = new_source_map();
185        sm.add_mapping(0, "a.rs", 1, 0, None);
186        sm.clear();
187        assert!(sm.is_empty());
188    }
189
190    #[test]
191    fn mapping_count() {
192        let mut sm = new_source_map();
193        sm.add_mapping(0, "a.rs", 1, 0, None);
194        sm.add_mapping(5, "a.rs", 2, 0, None);
195        assert_eq!(sm.mapping_count(), 2);
196    }
197}