backhopper_xref_reader/
scanner.rs1use backhopper_erlang_scan::{sigil_span, skip_char_literal_span, 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_sigil_or_advance(&mut self) {
131 debug_assert_eq!(self.peek(), Some(b'~'));
132 let span = sigil_span(self.bytes, self.pos.byte_offset as usize).unwrap_or(1);
133 for _ in 0..span {
134 self.advance();
135 }
136 }
137
138 pub fn consume_quoted_atom(&mut self) {
141 debug_assert_eq!(self.peek(), Some(b'\''));
142 self.advance();
143 while let Some(b) = self.peek() {
144 self.advance();
145 if b == b'\\' {
146 self.advance();
147 continue;
148 }
149 if b == b'\'' {
150 break;
151 }
152 }
153 }
154
155 pub fn consume_char_literal(&mut self) {
157 debug_assert_eq!(self.peek(), Some(b'$'));
158 let at = self.pos.byte_offset as usize;
159 let span = skip_char_literal_span(self.bytes, at);
160 for _ in 0..span {
162 self.advance();
163 }
164 }
165
166 pub fn consume_identifier(&mut self) -> &'a str {
169 let start = self.pos.byte_offset as usize;
170 while let Some(b) = self.peek() {
171 if b.is_ascii_alphanumeric() || b == b'_' || b == b'@' {
172 self.advance();
173 } else {
174 break;
175 }
176 }
177 let end = self.pos.byte_offset as usize;
178 &self.src[start..end]
179 }
180
181 pub fn consume_macro_ref(&mut self) -> &'a str {
183 debug_assert_eq!(self.peek(), Some(b'?'));
184 self.advance();
185 let start = self.pos.byte_offset as usize;
186 while let Some(b) = self.peek() {
187 if b.is_ascii_alphanumeric() || b == b'_' {
188 self.advance();
189 } else {
190 break;
191 }
192 }
193 let end = self.pos.byte_offset as usize;
194 &self.src[start..end]
195 }
196
197 pub fn skip_balanced_parens(&mut self) -> u32 {
200 debug_assert_eq!(self.peek(), Some(b'('));
201 self.advance();
202 let mut depth: i32 = 1;
203 let mut top_level_commas: u32 = 0;
204 while let Some(b) = self.peek() {
205 match b {
206 b'(' => {
207 depth += 1;
208 self.advance();
209 }
210 b')' => {
211 depth -= 1;
212 self.advance();
213 if depth == 0 {
214 return top_level_commas;
215 }
216 }
217 b'[' | b'{' => {
218 let opener = b;
220 let closer = if opener == b'[' { b']' } else { b'}' };
221 self.advance();
222 let mut inner = 1i32;
223 while let Some(b2) = self.peek() {
224 match b2 {
225 x if x == opener => {
226 inner += 1;
227 self.advance();
228 }
229 x if x == closer => {
230 inner -= 1;
231 self.advance();
232 if inner == 0 {
233 break;
234 }
235 }
236 b'"' => self.consume_string(),
237 b'~' => self.consume_sigil_or_advance(),
238 b'\'' => self.consume_quoted_atom(),
239 b'$' => self.consume_char_literal(),
240 b'%' => self.skip_line(),
241 _ => {
242 self.advance();
243 }
244 }
245 }
246 }
247 b',' if depth == 1 => {
248 top_level_commas += 1;
249 self.advance();
250 }
251 b'"' => self.consume_string(),
252 b'~' => self.consume_sigil_or_advance(),
253 b'\'' => self.consume_quoted_atom(),
254 b'$' => self.consume_char_literal(),
255 b'%' => self.skip_line(),
256 _ => {
257 self.advance();
258 }
259 }
260 }
261 top_level_commas
262 }
263}
264
265#[derive(Debug, Clone, Copy)]
267pub struct ParenWalk {
268 pub commas: u32,
269 pub had_content: bool,
270 pub closed: bool,
271}
272
273pub fn walk_paren_group(
282 sc: &mut Scanner<'_>,
283 mut on_ident: impl FnMut(&mut Scanner<'_>) -> bool,
284) -> ParenWalk {
285 debug_assert_eq!(sc.peek(), Some(b'('));
286 sc.advance();
287 let mut depth = 1i32;
288 let mut commas: u32 = 0;
289 let mut blocks = 0u32;
290 let mut had_content = false;
291 let mut closed = false;
292 let mut in_word = false;
293 while let Some(byte) = sc.peek() {
294 let at_word_start = !in_word;
295 in_word = byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'@');
296 if at_word_start && byte.is_ascii_lowercase() {
297 match block_effect(sc) {
298 BlockEffect::Open(len) => {
299 blocks += 1;
300 had_content = true;
301 advance_by(sc, len);
302 in_word = false;
303 continue;
304 }
305 BlockEffect::Close(len) => {
306 blocks = blocks.saturating_sub(1);
307 had_content = true;
308 advance_by(sc, len);
309 in_word = false;
310 continue;
311 }
312 BlockEffect::None => {}
313 }
314 }
315 match byte {
316 b'(' => {
317 depth += 1;
318 had_content = true;
319 sc.advance();
320 }
321 b')' => {
322 depth -= 1;
323 sc.advance();
324 if depth == 0 {
325 closed = true;
326 break;
327 }
328 }
329 b'[' | b'{' => {
330 depth += 1;
331 had_content = true;
332 sc.advance();
333 }
334 b']' | b'}' => {
335 depth -= 1;
336 sc.advance();
337 }
338 b'<' if sc.peek_at(1) == Some(b'<') => {
340 depth += 1;
341 had_content = true;
342 sc.advance();
343 sc.advance();
344 }
345 b'>' if sc.peek_at(1) == Some(b'>') => {
346 depth -= 1;
347 sc.advance();
348 sc.advance();
349 }
350 b',' if depth == 1 && blocks == 0 => {
351 commas += 1;
352 sc.advance();
353 }
354 b'"' => {
355 sc.consume_string();
356 had_content = true;
357 }
358 b'\'' => {
359 sc.consume_quoted_atom();
360 had_content = true;
361 }
362 b'$' => {
363 sc.consume_char_literal();
364 had_content = true;
365 }
366 b'%' => sc.skip_line(),
367 b'?' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
368 had_content = true;
369 if !on_ident(sc) {
370 sc.advance();
371 }
372 }
373 _ => {
374 if !byte.is_ascii_whitespace() {
375 had_content = true;
376 }
377 sc.advance();
378 }
379 }
380 }
381 ParenWalk {
382 commas,
383 had_content,
384 closed,
385 }
386}
387
388enum BlockEffect {
389 Open(usize),
390 Close(usize),
391 None,
392}
393
394const BLOCK_OPENERS: &[&str] = &["case", "if", "receive", "try", "begin", "maybe"];
395
396fn block_effect(sc: &mut Scanner<'_>) -> BlockEffect {
401 let start = sc.pos();
402 let word = sc.consume_identifier();
403 let len = word.len();
404 let effect = if word == "end" {
405 BlockEffect::Close(len)
406 } else if BLOCK_OPENERS.contains(&word) || (word == "fun" && opens_fun_expression(sc)) {
407 BlockEffect::Open(len)
408 } else {
409 BlockEffect::None
410 };
411 sc.set_pos(start);
412 effect
413}
414
415fn opens_fun_expression(sc: &mut Scanner<'_>) -> bool {
418 sc.skip_trivia();
419 if sc.peek() == Some(b'(') {
420 return true;
421 }
422 let name = sc.consume_identifier();
423 if name.is_empty() {
424 return false;
425 }
426 sc.skip_trivia();
427 sc.peek() == Some(b'(')
428}
429
430fn advance_by(sc: &mut Scanner<'_>, len: usize) {
431 for _ in 0..len {
432 sc.advance();
433 }
434}
435
436pub fn arity_from_commas(top_level_commas: u32, had_any_content: bool) -> u32 {
439 if had_any_content {
440 top_level_commas + 1
441 } else {
442 0
443 }
444}