backhopper_xref_reader/
scanner.rs1use backhopper_erlang_scan::triple_quoted_span;
15use backhopper_xref_graph::Position;
16
17#[derive(Debug, Clone)]
18pub struct Scanner<'a> {
19 src: &'a str,
20 bytes: &'a [u8],
21 pos: Position,
22}
23
24impl<'a> Scanner<'a> {
25 pub fn new(src: &'a str) -> Self {
26 Self {
27 src,
28 bytes: src.as_bytes(),
29 pos: Position::zero(),
30 }
31 }
32
33 pub fn pos(&self) -> Position {
34 self.pos
35 }
36
37 pub fn src(&self) -> &'a str {
38 self.src
39 }
40
41 pub fn src_slice(&self, start: usize, end: usize) -> &'a str {
42 &self.src[start..end]
43 }
44
45 pub fn set_pos(&mut self, pos: Position) {
47 self.pos = pos;
48 }
49
50 pub fn is_eof(&self) -> bool {
51 self.pos.byte_offset as usize >= self.bytes.len()
52 }
53
54 pub fn peek(&self) -> Option<u8> {
55 self.bytes.get(self.pos.byte_offset as usize).copied()
56 }
57
58 pub fn peek_at(&self, offset: usize) -> Option<u8> {
59 self.bytes
60 .get(self.pos.byte_offset as usize + offset)
61 .copied()
62 }
63
64 pub fn advance(&mut self) -> Option<u8> {
65 let p = self.pos.byte_offset as usize;
66 if p >= self.bytes.len() {
67 return None;
68 }
69 let b = self.bytes[p];
70 self.pos.byte_offset += 1;
71 if b == b'\n' {
72 self.pos.line += 1;
73 self.pos.column = 1;
74 } else {
75 self.pos.column += 1;
76 }
77 Some(b)
78 }
79
80 pub fn skip_line(&mut self) {
81 while let Some(b) = self.peek() {
82 self.advance();
83 if b == b'\n' {
84 break;
85 }
86 }
87 }
88
89 pub fn skip_trivia(&mut self) {
92 loop {
93 match self.peek() {
94 Some(b) if b.is_ascii_whitespace() => {
95 self.advance();
96 }
97 Some(b'%') => self.skip_line(),
98 _ => break,
99 }
100 }
101 }
102
103 pub fn consume_string(&mut self) {
108 debug_assert_eq!(self.peek(), Some(b'"'));
109 if let Some(span) = triple_quoted_span(self.bytes, self.pos.byte_offset as usize) {
110 for _ in 0..span {
111 self.advance();
112 }
113 return;
114 }
115 self.advance();
116 while let Some(b) = self.peek() {
117 self.advance();
118 if b == b'\\' {
119 self.advance();
120 continue;
121 }
122 if b == b'"' {
123 break;
124 }
125 }
126 }
127
128 pub fn consume_quoted_atom(&mut self) {
131 debug_assert_eq!(self.peek(), Some(b'\''));
132 self.advance();
133 while let Some(b) = self.peek() {
134 self.advance();
135 if b == b'\\' {
136 self.advance();
137 continue;
138 }
139 if b == b'\'' {
140 break;
141 }
142 }
143 }
144
145 pub fn consume_char_literal(&mut self) {
147 debug_assert_eq!(self.peek(), Some(b'$'));
148 self.advance();
149 if self.peek() == Some(b'\\') {
150 self.advance();
151 if self.peek() == Some(b'^') {
152 self.advance();
153 }
154 self.advance();
155 } else {
156 self.advance();
157 }
158 }
159
160 pub fn consume_identifier(&mut self) -> &'a str {
163 let start = self.pos.byte_offset as usize;
164 while let Some(b) = self.peek() {
165 if b.is_ascii_alphanumeric() || b == b'_' || b == b'@' {
166 self.advance();
167 } else {
168 break;
169 }
170 }
171 let end = self.pos.byte_offset as usize;
172 &self.src[start..end]
173 }
174
175 pub fn consume_macro_ref(&mut self) -> &'a str {
177 debug_assert_eq!(self.peek(), Some(b'?'));
178 self.advance();
179 let start = self.pos.byte_offset as usize;
180 while let Some(b) = self.peek() {
181 if b.is_ascii_alphanumeric() || b == b'_' {
182 self.advance();
183 } else {
184 break;
185 }
186 }
187 let end = self.pos.byte_offset as usize;
188 &self.src[start..end]
189 }
190
191 pub fn skip_balanced_parens(&mut self) -> u32 {
194 debug_assert_eq!(self.peek(), Some(b'('));
195 self.advance();
196 let mut depth: i32 = 1;
197 let mut top_level_commas: u32 = 0;
198 while let Some(b) = self.peek() {
199 match b {
200 b'(' => {
201 depth += 1;
202 self.advance();
203 }
204 b')' => {
205 depth -= 1;
206 self.advance();
207 if depth == 0 {
208 return top_level_commas;
209 }
210 }
211 b'[' | b'{' => {
212 let opener = b;
214 let closer = if opener == b'[' { b']' } else { b'}' };
215 self.advance();
216 let mut inner = 1i32;
217 while let Some(b2) = self.peek() {
218 match b2 {
219 x if x == opener => {
220 inner += 1;
221 self.advance();
222 }
223 x if x == closer => {
224 inner -= 1;
225 self.advance();
226 if inner == 0 {
227 break;
228 }
229 }
230 b'"' => self.consume_string(),
231 b'\'' => self.consume_quoted_atom(),
232 b'$' => self.consume_char_literal(),
233 b'%' => self.skip_line(),
234 _ => {
235 self.advance();
236 }
237 }
238 }
239 }
240 b',' if depth == 1 => {
241 top_level_commas += 1;
242 self.advance();
243 }
244 b'"' => self.consume_string(),
245 b'\'' => self.consume_quoted_atom(),
246 b'$' => self.consume_char_literal(),
247 b'%' => self.skip_line(),
248 _ => {
249 self.advance();
250 }
251 }
252 }
253 top_level_commas
254 }
255}
256
257#[derive(Debug, Clone, Copy)]
259pub struct ParenWalk {
260 pub commas: u32,
261 pub had_content: bool,
262 pub closed: bool,
263}
264
265pub fn walk_paren_group(
274 sc: &mut Scanner<'_>,
275 mut on_ident: impl FnMut(&mut Scanner<'_>) -> bool,
276) -> ParenWalk {
277 debug_assert_eq!(sc.peek(), Some(b'('));
278 sc.advance();
279 let mut depth = 1i32;
280 let mut commas: u32 = 0;
281 let mut had_content = false;
282 let mut closed = false;
283 while let Some(byte) = sc.peek() {
284 match byte {
285 b'(' => {
286 depth += 1;
287 had_content = true;
288 sc.advance();
289 }
290 b')' => {
291 depth -= 1;
292 sc.advance();
293 if depth == 0 {
294 closed = true;
295 break;
296 }
297 }
298 b'[' | b'{' => {
299 depth += 1;
300 had_content = true;
301 sc.advance();
302 }
303 b']' | b'}' => {
304 depth -= 1;
305 sc.advance();
306 }
307 b'<' if sc.peek_at(1) == Some(b'<') => {
309 depth += 1;
310 had_content = true;
311 sc.advance();
312 sc.advance();
313 }
314 b'>' if sc.peek_at(1) == Some(b'>') => {
315 depth -= 1;
316 sc.advance();
317 sc.advance();
318 }
319 b',' if depth == 1 => {
320 commas += 1;
321 sc.advance();
322 }
323 b'"' => {
324 sc.consume_string();
325 had_content = true;
326 }
327 b'\'' => {
328 sc.consume_quoted_atom();
329 had_content = true;
330 }
331 b'$' => {
332 sc.consume_char_literal();
333 had_content = true;
334 }
335 b'%' => sc.skip_line(),
336 b'?' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
337 had_content = true;
338 if !on_ident(sc) {
339 sc.advance();
340 }
341 }
342 _ => {
343 if !byte.is_ascii_whitespace() {
344 had_content = true;
345 }
346 sc.advance();
347 }
348 }
349 }
350 ParenWalk {
351 commas,
352 had_content,
353 closed,
354 }
355}
356
357pub fn arity_from_commas(top_level_commas: u32, had_any_content: bool) -> u32 {
360 if had_any_content {
361 top_level_commas + 1
362 } else {
363 0
364 }
365}