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 had_content = false;
290 let mut closed = false;
291 while let Some(byte) = sc.peek() {
292 match byte {
293 b'(' => {
294 depth += 1;
295 had_content = true;
296 sc.advance();
297 }
298 b')' => {
299 depth -= 1;
300 sc.advance();
301 if depth == 0 {
302 closed = true;
303 break;
304 }
305 }
306 b'[' | b'{' => {
307 depth += 1;
308 had_content = true;
309 sc.advance();
310 }
311 b']' | b'}' => {
312 depth -= 1;
313 sc.advance();
314 }
315 b'<' if sc.peek_at(1) == Some(b'<') => {
317 depth += 1;
318 had_content = true;
319 sc.advance();
320 sc.advance();
321 }
322 b'>' if sc.peek_at(1) == Some(b'>') => {
323 depth -= 1;
324 sc.advance();
325 sc.advance();
326 }
327 b',' if depth == 1 => {
328 commas += 1;
329 sc.advance();
330 }
331 b'"' => {
332 sc.consume_string();
333 had_content = true;
334 }
335 b'\'' => {
336 sc.consume_quoted_atom();
337 had_content = true;
338 }
339 b'$' => {
340 sc.consume_char_literal();
341 had_content = true;
342 }
343 b'%' => sc.skip_line(),
344 b'?' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
345 had_content = true;
346 if !on_ident(sc) {
347 sc.advance();
348 }
349 }
350 _ => {
351 if !byte.is_ascii_whitespace() {
352 had_content = true;
353 }
354 sc.advance();
355 }
356 }
357 }
358 ParenWalk {
359 commas,
360 had_content,
361 closed,
362 }
363}
364
365pub fn arity_from_commas(top_level_commas: u32, had_any_content: bool) -> u32 {
368 if had_any_content {
369 top_level_commas + 1
370 } else {
371 0
372 }
373}