php_rs_parser/
source_map.rs1use php_ast::Span;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct LineCol {
24 pub line: u32,
26 pub col: u32,
28}
29
30impl LineCol {
31 pub fn to_one_based(self) -> (u32, u32) {
33 (self.line + 1, self.col + 1)
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct LineColSpan {
40 pub start: LineCol,
42 pub end: LineCol,
44}
45
46pub struct SourceMap {
51 line_starts: Vec<u32>,
53}
54
55impl SourceMap {
56 pub fn new(source: &str) -> Self {
58 let mut line_starts = Vec::with_capacity(source.len() / 40 + 1);
59 line_starts.push(0u32);
60 for pos in memchr::memchr_iter(b'\n', source.as_bytes()) {
61 line_starts.push((pos + 1) as u32);
62 }
63 Self { line_starts }
64 }
65
66 pub fn empty() -> Self {
70 Self {
71 line_starts: vec![0u32],
72 }
73 }
74
75 pub fn line_count(&self) -> usize {
77 self.line_starts.len()
78 }
79
80 pub fn line_start(&self, line: u32) -> Option<u32> {
83 self.line_starts.get(line as usize).copied()
84 }
85
86 pub fn offset_to_line_col(&self, offset: u32) -> LineCol {
91 let line = match self.line_starts.binary_search(&offset) {
92 Ok(exact) => exact,
93 Err(after) => after - 1,
94 };
95 let col = offset - self.line_starts[line];
96 LineCol {
97 line: line as u32,
98 col,
99 }
100 }
101
102 pub fn span_to_line_col(&self, span: Span) -> LineColSpan {
104 LineColSpan {
105 start: self.offset_to_line_col(span.start),
106 end: self.offset_to_line_col(span.end),
107 }
108 }
109
110 pub fn line_col_to_offset(&self, lc: LineCol) -> Option<u32> {
113 self.line_starts
114 .get(lc.line as usize)
115 .map(|start| start + lc.col)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn empty_source() {
125 let map = SourceMap::new("");
126 assert_eq!(map.line_count(), 1);
127 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
128 }
129
130 #[test]
131 fn single_line_no_newline() {
132 let map = SourceMap::new("<?php echo 1;");
133 assert_eq!(map.line_count(), 1);
134 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
135 assert_eq!(map.offset_to_line_col(6), LineCol { line: 0, col: 6 });
136 }
137
138 #[test]
139 fn multiple_lines() {
140 let src = "<?php\necho 'hi';\nreturn;\n";
141 let map = SourceMap::new(src);
142 assert_eq!(map.line_count(), 4); assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
146 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
148 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
150 assert_eq!(map.offset_to_line_col(17), LineCol { line: 2, col: 0 });
152 }
153
154 #[test]
155 fn span_conversion() {
156 let src = "<?php\necho 'hi';\n";
157 let map = SourceMap::new(src);
158 let span = Span::new(6, 10); let lc = map.span_to_line_col(span);
160 assert_eq!(lc.start, LineCol { line: 1, col: 0 });
161 assert_eq!(lc.end, LineCol { line: 1, col: 4 });
162 }
163
164 #[test]
165 fn round_trip() {
166 let src = "<?php\necho 'hi';\nreturn;\n";
167 let map = SourceMap::new(src);
168 let lc = LineCol { line: 1, col: 5 };
169 let offset = map.line_col_to_offset(lc).unwrap();
170 assert_eq!(map.offset_to_line_col(offset), lc);
171 }
172
173 #[test]
174 fn one_based() {
175 let lc = LineCol { line: 0, col: 0 };
176 assert_eq!(lc.to_one_based(), (1, 1));
177 let lc = LineCol { line: 2, col: 5 };
178 assert_eq!(lc.to_one_based(), (3, 6));
179 }
180
181 #[test]
182 fn line_start_lookup() {
183 let src = "aaa\nbbb\nccc";
184 let map = SourceMap::new(src);
185 assert_eq!(map.line_start(0), Some(0));
186 assert_eq!(map.line_start(1), Some(4));
187 assert_eq!(map.line_start(2), Some(8));
188 assert_eq!(map.line_start(3), None);
189 }
190
191 #[test]
192 fn crlf_treated_as_two_bytes() {
193 let src = "a\r\nb";
195 let map = SourceMap::new(src);
196 assert_eq!(map.line_count(), 2);
197 assert_eq!(map.offset_to_line_col(3), LineCol { line: 1, col: 0 });
199 }
200}