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,
41 pub end: LineCol,
42}
43
44pub struct SourceMap {
49 line_starts: Vec<u32>,
51}
52
53impl SourceMap {
54 pub fn new(source: &str) -> Self {
56 let mut line_starts = Vec::with_capacity(source.len() / 40 + 1);
57 line_starts.push(0u32);
58 for pos in memchr::memchr_iter(b'\n', source.as_bytes()) {
59 line_starts.push((pos + 1) as u32);
60 }
61 Self { line_starts }
62 }
63
64 pub fn empty() -> Self {
68 Self {
69 line_starts: vec![0u32],
70 }
71 }
72
73 pub fn line_count(&self) -> usize {
75 self.line_starts.len()
76 }
77
78 pub fn line_start(&self, line: u32) -> Option<u32> {
81 self.line_starts.get(line as usize).copied()
82 }
83
84 pub fn offset_to_line_col(&self, offset: u32) -> LineCol {
89 let line = match self.line_starts.binary_search(&offset) {
90 Ok(exact) => exact,
91 Err(after) => after - 1,
92 };
93 let col = offset - self.line_starts[line];
94 LineCol {
95 line: line as u32,
96 col,
97 }
98 }
99
100 pub fn span_to_line_col(&self, span: Span) -> LineColSpan {
102 LineColSpan {
103 start: self.offset_to_line_col(span.start),
104 end: self.offset_to_line_col(span.end),
105 }
106 }
107
108 pub fn line_col_to_offset(&self, lc: LineCol) -> Option<u32> {
111 self.line_starts
112 .get(lc.line as usize)
113 .map(|start| start + lc.col)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn empty_source() {
123 let map = SourceMap::new("");
124 assert_eq!(map.line_count(), 1);
125 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
126 }
127
128 #[test]
129 fn single_line_no_newline() {
130 let map = SourceMap::new("<?php echo 1;");
131 assert_eq!(map.line_count(), 1);
132 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
133 assert_eq!(map.offset_to_line_col(6), LineCol { line: 0, col: 6 });
134 }
135
136 #[test]
137 fn multiple_lines() {
138 let src = "<?php\necho 'hi';\nreturn;\n";
139 let map = SourceMap::new(src);
140 assert_eq!(map.line_count(), 4); assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
144 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
146 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
148 assert_eq!(map.offset_to_line_col(17), LineCol { line: 2, col: 0 });
150 }
151
152 #[test]
153 fn span_conversion() {
154 let src = "<?php\necho 'hi';\n";
155 let map = SourceMap::new(src);
156 let span = Span::new(6, 10); let lc = map.span_to_line_col(span);
158 assert_eq!(lc.start, LineCol { line: 1, col: 0 });
159 assert_eq!(lc.end, LineCol { line: 1, col: 4 });
160 }
161
162 #[test]
163 fn round_trip() {
164 let src = "<?php\necho 'hi';\nreturn;\n";
165 let map = SourceMap::new(src);
166 let lc = LineCol { line: 1, col: 5 };
167 let offset = map.line_col_to_offset(lc).unwrap();
168 assert_eq!(map.offset_to_line_col(offset), lc);
169 }
170
171 #[test]
172 fn one_based() {
173 let lc = LineCol { line: 0, col: 0 };
174 assert_eq!(lc.to_one_based(), (1, 1));
175 let lc = LineCol { line: 2, col: 5 };
176 assert_eq!(lc.to_one_based(), (3, 6));
177 }
178
179 #[test]
180 fn line_start_lookup() {
181 let src = "aaa\nbbb\nccc";
182 let map = SourceMap::new(src);
183 assert_eq!(map.line_start(0), Some(0));
184 assert_eq!(map.line_start(1), Some(4));
185 assert_eq!(map.line_start(2), Some(8));
186 assert_eq!(map.line_start(3), None);
187 }
188
189 #[test]
190 fn crlf_treated_as_two_bytes() {
191 let src = "a\r\nb";
193 let map = SourceMap::new(src);
194 assert_eq!(map.line_count(), 2);
195 assert_eq!(map.offset_to_line_col(3), LineCol { line: 1, col: 0 });
197 }
198}