1use std::mem;
22use std::str::FromStr;
23use std::sync::OnceLock;
24
25use regex::Regex;
26
27use crate::compat::arg_shape::ArgShape;
28use crate::erlang_macros::{MacroTable, expand_value_macro_to_atom, expand_value_macro_to_mf};
29use crate::model::names::{Arity, FunctionName, Mfa, ModuleName, RecordName, TypeName};
30use crate::model::symbol::{RefContext, RefOrigin, SymbolRef};
31
32pub use backhopper_erlang_scan::{
35 ScanArity, ScannedArgs, scan_top_level_args, split_top_level_args,
36};
37use backhopper_erlang_scan::{count_top_level_items, scan_arity};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum DynamicCall {
41 Apply,
44 VariableDispatch,
47}
48
49pub(crate) fn call_re() -> &'static Regex {
50 static R: OnceLock<Regex> = OnceLock::new();
51 R.get_or_init(|| {
52 Regex::new(r"\b([a-z][a-zA-Z0-9_@]*)\s*:\s*([a-z][a-zA-Z0-9_@]*)\s*\(").expect("regex")
53 })
54}
55
56fn record_re() -> &'static Regex {
57 static R: OnceLock<Regex> = OnceLock::new();
58 R.get_or_init(|| Regex::new(r"#([a-z][a-zA-Z0-9_@]*)\b").expect("regex"))
59}
60
61fn macro_re() -> &'static Regex {
62 static R: OnceLock<Regex> = OnceLock::new();
63 R.get_or_init(|| Regex::new(r"\?([A-Z_][A-Za-z0-9_]*)\b").expect("regex"))
64}
65
66fn fun_def_re() -> &'static Regex {
69 static R: OnceLock<Regex> = OnceLock::new();
70 R.get_or_init(|| Regex::new(r"(?m)^([a-z][a-zA-Z0-9_@]*)\s*\(").expect("regex"))
71}
72
73fn apply_bif_re() -> &'static Regex {
74 static R: OnceLock<Regex> = OnceLock::new();
75 R.get_or_init(|| {
76 Regex::new(r"\b(apply|spawn|spawn_link|spawn_monitor|spawn_opt|hibernate)\s*\(")
77 .expect("regex")
78 })
79}
80
81fn var_module_call_re() -> &'static Regex {
82 static R: OnceLock<Regex> = OnceLock::new();
83 R.get_or_init(|| {
84 Regex::new(r"\b[A-Z][a-zA-Z0-9_@]*\s*:\s*[a-zA-Z_][a-zA-Z0-9_@]*\s*\(").expect("regex")
85 })
86}
87
88fn var_function_call_re() -> &'static Regex {
89 static R: OnceLock<Regex> = OnceLock::new();
90 R.get_or_init(|| {
91 Regex::new(r"\b[a-z][a-zA-Z0-9_@]*\s*:\s*[A-Z][a-zA-Z0-9_@]*\s*\(").expect("regex")
92 })
93}
94
95pub fn extract_into(line: &str, out: &mut Vec<SymbolRef>) {
96 extract_into_with_macros(line, empty_macros(), out);
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct QualifiedCallSite {
102 pub mfa: Mfa,
103 pub line: u32,
104}
105
106pub fn extract_qualified_calls(src: &str, line_map: &[u32]) -> Vec<QualifiedCallSite> {
117 let mut out = Vec::new();
118 for run in body_runs(src, line_map) {
119 let mut calls = Vec::new();
120 push_call_args_with_offsets(&run.text, &mut calls);
121 for ((mfa, _shapes), offset) in calls {
122 out.push(QualifiedCallSite {
123 mfa,
124 line: run_line_at(&run.line_starts, offset),
125 });
126 }
127 }
128 out
129}
130
131#[derive(Debug)]
136pub(crate) struct BodyRun {
137 pub(crate) text: String,
138 pub(crate) line_starts: Vec<(usize, u32)>,
139}
140
141pub(crate) fn body_runs_with<'a>(
147 src: &'a str,
148 line_map: &[u32],
149 strip: impl Fn(&'a str) -> &'a str,
150 mut is_body: impl FnMut(&str) -> bool,
151) -> Vec<BodyRun> {
152 debug_assert_eq!(
153 line_map.len(),
154 src.lines().count(),
155 "line map length must equal the line count"
156 );
157 let mut out = Vec::new();
158 let mut run = String::new();
159 let mut run_lines: Vec<(usize, u32)> = Vec::new();
160 let mut prev_body_file_line: Option<u32> = None;
161 for (idx, line) in src.lines().enumerate() {
162 let stripped = strip(line);
163 let body = is_body(stripped);
164 let file_line = line_map.get(idx).copied();
165 let adjacent =
166 matches!((prev_body_file_line, file_line), (Some(prev), Some(cur)) if cur == prev + 1);
167 if (!body || !adjacent) && !run.is_empty() {
168 out.push(BodyRun {
169 text: mem::take(&mut run),
170 line_starts: mem::take(&mut run_lines),
171 });
172 }
173 prev_body_file_line = body.then_some(file_line).flatten();
174 if !body {
175 continue;
176 }
177 run_lines.push((run.len(), (idx + 1) as u32));
178 run.push_str(stripped);
179 run.push('\n');
180 }
181 if !run.is_empty() {
182 out.push(BodyRun {
183 text: run,
184 line_starts: run_lines,
185 });
186 }
187 out
188}
189
190pub(crate) fn body_runs(src: &str, line_map: &[u32]) -> Vec<BodyRun> {
193 let mut scanner = AttrCtxScanner::new();
194 body_runs_with(src, line_map, strip_line_comment, |s| {
195 scanner.classify(s) == RefContext::Body
196 })
197}
198
199fn value_at_or_before<T: Copy>(entries: &[(usize, T)], offset: usize, default: T) -> T {
203 match entries.partition_point(|(start, _)| *start <= offset) {
204 0 => default,
205 count => entries[count - 1].1,
206 }
207}
208
209fn scanned_call(scan: ScanArity, module: ModuleName, function: FunctionName) -> SymbolRef {
212 match scan {
213 ScanArity::Exact(arity) => {
214 SymbolRef::function(Mfa::new(module, function, Arity::new(arity)))
215 }
216 ScanArity::Unterminated => SymbolRef::function_any_arity(module, function),
217 }
218}
219
220pub(crate) fn run_line_at(run_lines: &[(usize, u32)], offset: usize) -> u32 {
224 value_at_or_before(run_lines, offset, run_lines.first().map_or(1, |(_, l)| *l))
225}
226
227#[derive(Debug, Default, Clone, Copy)]
235pub struct AttrCtxScanner {
236 in_attr: bool,
237}
238
239impl AttrCtxScanner {
240 pub fn new() -> Self {
241 Self { in_attr: false }
242 }
243
244 pub fn classify(&mut self, line: &str) -> RefContext {
245 if self.in_attr {
246 if line_closes_attribute(line) {
247 self.in_attr = false;
248 }
249 return RefContext::TypeAttribute;
250 }
251 if line_opens_type_attr(line) {
252 if !line_closes_attribute(line) {
253 self.in_attr = true;
254 }
255 return RefContext::TypeAttribute;
256 }
257 RefContext::Body
258 }
259}
260
261fn line_opens_type_attr(line: &str) -> bool {
262 let trimmed = line.trim_start();
263 if !trimmed.starts_with('-') {
264 return false;
265 }
266 for prefix in ["-spec", "-callback", "-type", "-opaque"] {
267 if let Some(rest) = trimmed.strip_prefix(prefix) {
268 match rest.chars().next() {
269 Some(c) if c == '(' || c.is_whitespace() => return true,
270 _ => {}
271 }
272 }
273 }
274 false
275}
276
277fn line_closes_attribute(line: &str) -> bool {
278 let stripped = strip_line_comment(line);
279 stripped.trim_end().ends_with('.')
280}
281
282pub fn strip_line_comment(line: &str) -> &str {
285 let bytes = line.as_bytes();
286 let mut in_str = false;
287 let mut in_atom = false;
288 let mut i = 0usize;
289 while i < bytes.len() {
290 let b = bytes[i];
291 if in_str || in_atom {
292 match b {
293 b'\\' => i += 1,
294 b'"' if in_str => in_str = false,
295 b'\'' if in_atom => in_atom = false,
296 _ => {}
297 }
298 i += 1;
299 continue;
300 }
301 match b {
302 b'"' => in_str = true,
303 b'\'' => in_atom = true,
304 b'$' => {
305 i += 1;
306 if bytes.get(i) == Some(&b'\\') {
307 i += 1;
308 }
309 }
310 b'%' => return &line[..i],
311 _ => {}
312 }
313 i += 1;
314 }
315 line
316}
317
318pub fn extract_type_refs_into(line: &str, out: &mut Vec<SymbolRef>) {
329 let line = strip_line_comment(line);
330 let mut with_offsets = Vec::new();
331 push_type_refs_with_offsets(line, &mut with_offsets);
332 out.extend(with_offsets.into_iter().map(|(r, _)| r));
333}
334
335fn push_type_refs_with_offsets(text: &str, out: &mut Vec<(SymbolRef, usize)>) {
338 for caps in call_re().captures_iter(text) {
339 let group = caps.get(0).expect("capture");
340 let module = &caps[1];
341 let ident = &caps[2];
342 let ScanArity::Exact(arity) = scan_arity(&text[group.end()..]) else {
345 continue;
346 };
347 if let (Ok(m), Ok(t)) = (ModuleName::from_str(module), TypeName::from_str(ident)) {
348 out.push((SymbolRef::type_ref(m, t, Arity::new(arity)), group.start()));
349 }
350 }
351 for caps in macro_re().captures_iter(text) {
352 let group = caps.get(0).expect("capture");
353 out.push((SymbolRef::macro_use(caps[1].to_owned()), group.start()));
354 }
355}
356
357fn empty_macros() -> &'static MacroTable {
358 static EMPTY: OnceLock<MacroTable> = OnceLock::new();
359 EMPTY.get_or_init(MacroTable::new)
360}
361
362pub fn extract_into_with_macros(line: &str, macros: &MacroTable, out: &mut Vec<SymbolRef>) {
368 let line = strip_line_comment(line);
369 let mut with_offsets = Vec::new();
370 push_refs_with_offsets(line, macros, &mut with_offsets);
371 out.extend(with_offsets.into_iter().map(|(r, _)| r));
372}
373
374fn push_refs_with_offsets(text: &str, macros: &MacroTable, out: &mut Vec<(SymbolRef, usize)>) {
378 for caps in call_re().captures_iter(text) {
379 let group = caps.get(0).expect("capture");
380 let module = &caps[1];
381 let function = &caps[2];
382 let after = &text[group.end()..];
383 let (Ok(m), Ok(f)) = (
384 ModuleName::from_str(module),
385 FunctionName::from_str(function),
386 ) else {
387 continue;
388 };
389 out.push((scanned_call(scan_arity(after), m, f), group.start()));
390 }
391 push_fun_refs_with_offsets(text, macros, out);
392 for caps in record_re().captures_iter(text) {
393 let start = caps.get(0).expect("capture").start();
394 if let Ok(name) = RecordName::from_str(&caps[1]) {
395 out.push((SymbolRef::record(name), start));
396 }
397 }
398 for caps in macro_re().captures_iter(text) {
399 let group = caps.get(0).expect("capture");
400 let name = &caps[1];
401 out.push((SymbolRef::macro_use(name.to_owned()), group.start()));
402 let mut resolved = Vec::new();
403 try_resolve_macro_call(name, &text[group.end()..], macros, &mut resolved);
404 out.extend(resolved.into_iter().map(|r| (r, group.start())));
405 }
406 push_apply_family_with_offsets(text, macros, out);
407}
408
409fn fun_ref_re() -> &'static Regex {
410 static R: OnceLock<Regex> = OnceLock::new();
411 R.get_or_init(|| {
412 Regex::new(
413 r"\bfun\s+(\??[A-Za-z_][a-zA-Z0-9_@]*)\s*:\s*([A-Za-z_][a-zA-Z0-9_@]*)\s*/\s*([0-9]+|[A-Za-z_][a-zA-Z0-9_@]*)",
414 )
415 .expect("regex")
416 })
417}
418
419fn push_fun_refs_with_offsets(text: &str, macros: &MacroTable, out: &mut Vec<(SymbolRef, usize)>) {
423 for caps in fun_ref_re().captures_iter(text) {
424 let module_raw = &caps[1];
425 let function_raw = &caps[2];
426 let arity_raw = &caps[3];
427 let Ok(arity) = arity_raw.parse::<u8>() else {
428 continue;
429 };
430 if !function_raw.starts_with(|c: char| c.is_ascii_lowercase()) {
431 continue;
432 }
433 let module = if let Some(macro_name) = module_raw.strip_prefix('?') {
434 let Some(expanded) = expand_value_macro_to_atom(macros, macro_name) else {
435 continue;
436 };
437 let Ok(m) = ModuleName::from_str(&expanded) else {
438 continue;
439 };
440 m
441 } else if module_raw.starts_with(|c: char| c.is_ascii_lowercase()) {
442 let Ok(m) = ModuleName::from_str(module_raw) else {
443 continue;
444 };
445 m
446 } else {
447 continue;
448 };
449 let Ok(f) = FunctionName::from_str(function_raw) else {
450 continue;
451 };
452 let start = caps.get(0).expect("capture").start();
453 out.push((
454 SymbolRef::function(Mfa::new(module, f, Arity::new(arity))),
455 start,
456 ));
457 }
458}
459
460fn try_resolve_macro_call(name: &str, tail: &str, macros: &MacroTable, out: &mut Vec<SymbolRef>) {
461 let trimmed = tail.trim_start();
462 if let Some(rest) = trimmed.strip_prefix('(') {
463 let Some((m, f)) = expand_value_macro_to_mf(macros, name) else {
464 return;
465 };
466 let (Ok(mn), Ok(fn_)) = (ModuleName::from_str(&m), FunctionName::from_str(&f)) else {
467 return;
468 };
469 out.push(scanned_call(scan_arity(rest), mn, fn_));
470 return;
471 }
472 if let Some(rest) = trimmed.strip_prefix(':') {
473 let rest = rest.trim_start();
474 let (function_str, after_fn) = split_lowercase_ident(rest);
475 if function_str.is_empty() {
476 return;
477 }
478 let after = after_fn.trim_start();
479 if let Some(after_open) = after.strip_prefix('(') {
480 if let Some(m) = expand_value_macro_to_atom(macros, name) {
481 if let (Ok(mn), Ok(fn_)) = (
482 ModuleName::from_str(&m),
483 FunctionName::from_str(function_str),
484 ) {
485 out.push(scanned_call(scan_arity(after_open), mn, fn_));
486 }
487 }
488 }
489 }
490}
491
492fn split_lowercase_ident(s: &str) -> (&str, &str) {
493 let end = s
494 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '@'))
495 .unwrap_or(s.len());
496 if end == 0 || !s.starts_with(|c: char| c.is_ascii_lowercase()) {
497 return ("", s);
498 }
499 s.split_at(end)
500}
501
502fn push_apply_family_with_offsets(
503 text: &str,
504 macros: &MacroTable,
505 out: &mut Vec<(SymbolRef, usize)>,
506) {
507 for caps in apply_family_re().captures_iter(text) {
508 let group = caps.get(0).expect("capture");
509 let name = &caps[1];
510 let after_open = &text[group.end()..];
511 let ScannedArgs::Terminated { args, .. } = scan_top_level_args(after_open) else {
514 continue;
515 };
516 if let Some(mfa) = resolve_apply_family(name, &args, macros) {
517 out.push((SymbolRef::function(mfa), group.start()));
518 }
519 }
520}
521
522fn apply_family_re() -> &'static Regex {
523 static R: OnceLock<Regex> = OnceLock::new();
524 R.get_or_init(|| {
525 Regex::new(
526 r"\b(?:erlang\s*:\s*)?(apply|spawn|spawn_link|spawn_monitor|spawn_opt|hibernate)\s*\(",
527 )
528 .expect("regex")
529 })
530}
531
532fn resolve_apply_family(name: &str, args: &[&str], macros: &MacroTable) -> Option<Mfa> {
533 let (m_raw, f_raw, a_raw) = match (name, args.len()) {
534 ("apply", 3)
535 | ("spawn" | "spawn_link" | "spawn_monitor" | "hibernate", 3)
536 | ("spawn_opt", 4) => (args[0], args[1], args[2]),
537 ("spawn" | "spawn_link" | "spawn_monitor", 4) | ("spawn_opt", 5) => {
538 (args[1], args[2], args[3])
539 }
540 _ => return None,
541 };
542 let module = atom_or_macro_to::<ModuleName>(m_raw, macros)?;
543 let function = atom_or_macro_to::<FunctionName>(f_raw, macros)?;
544 let arity = literal_list_length(a_raw)?;
545 Some(Mfa::new(module, function, Arity::new(arity)))
546}
547
548fn atom_or_macro_to<T: FromStr>(raw: &str, macros: &MacroTable) -> Option<T> {
550 let s = raw.trim();
551 if let Some(name) = s.strip_prefix('?') {
552 return expand_value_macro_to_atom(macros, name).and_then(|a| T::from_str(&a).ok());
553 }
554 if looks_like_lowercase_atom(s) {
555 return T::from_str(s).ok();
556 }
557 if s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2 {
558 return T::from_str(&s[1..s.len() - 1]).ok();
559 }
560 None
561}
562
563fn literal_list_length(raw: &str) -> Option<u8> {
564 let s = raw.trim();
565 let inner = s.strip_prefix('[')?.strip_suffix(']')?;
566 let trimmed = inner.trim();
567 if trimmed.is_empty() {
568 return Some(0);
569 }
570 u8::try_from(count_top_level_items(s, '[', ']')).ok()
571}
572
573fn looks_like_lowercase_atom(s: &str) -> bool {
574 let mut chars = s.chars();
575 match chars.next() {
576 Some(c) if c.is_ascii_lowercase() => {}
577 _ => return false,
578 }
579 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
580}
581
582pub fn extract_call_args_into(line: &str, out: &mut Vec<(Mfa, Vec<ArgShape>)>) {
587 let line = strip_line_comment(line);
588 let mut with_offsets = Vec::new();
589 push_call_args_with_offsets(line, &mut with_offsets);
590 out.extend(with_offsets.into_iter().map(|(c, _)| c));
591}
592
593fn push_call_args_with_offsets(text: &str, out: &mut Vec<((Mfa, Vec<ArgShape>), usize)>) {
596 for caps in call_re().captures_iter(text) {
597 let group = caps.get(0).expect("capture");
598 let module = &caps[1];
599 let function = &caps[2];
600 let Some((args, _consumed)) = extract_arg_shapes(&text[group.end()..]) else {
603 continue;
604 };
605 let arity = u8::try_from(args.len()).unwrap_or(u8::MAX);
606 if let (Ok(m), Ok(f)) = (
607 ModuleName::from_str(module),
608 FunctionName::from_str(function),
609 ) {
610 out.push(((Mfa::new(m, f, Arity::new(arity)), args), group.start()));
611 }
612 }
613}
614
615fn extract_arg_shapes(after_open_paren: &str) -> Option<(Vec<ArgShape>, usize)> {
616 match scan_top_level_args(after_open_paren) {
617 ScannedArgs::Terminated { args, consumed } => {
618 Some((args.iter().map(|a| classify_arg(a)).collect(), consumed))
619 }
620 ScannedArgs::Unterminated { .. } => None,
621 }
622}
623
624pub fn classify_arg(raw: &str) -> ArgShape {
625 let s = raw.trim();
626 if s.is_empty() {
627 return ArgShape::Unknown;
628 }
629 let first = s.chars().next().expect("non-empty");
630 if first == '#' {
631 let rest = &s[1..];
632 let name_end = rest
633 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '@'))
634 .unwrap_or(rest.len());
635 let name = &rest[..name_end];
636 if let Ok(record_name) = RecordName::from_str(name) {
637 return ArgShape::Record { name: record_name };
638 }
639 return ArgShape::Unknown;
640 }
641 if first == '{' {
642 let size = count_top_level_items(s, '{', '}');
643 return ArgShape::Tuple { size };
644 }
645 if first == '[' {
646 return ArgShape::List;
647 }
648 if first == '"' {
649 return ArgShape::String;
650 }
651 if first == '<' && s.starts_with("<<") {
652 return ArgShape::Binary;
653 }
654 if first == '\'' {
655 let inner = s.trim_start_matches('\'').trim_end_matches('\'');
656 return ArgShape::Atom {
657 name: inner.to_string(),
658 };
659 }
660 if first.is_ascii_digit()
661 || (first == '-' && s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
662 {
663 return if s.contains('.') {
664 ArgShape::Float
665 } else {
666 ArgShape::Integer
667 };
668 }
669 if first.is_ascii_uppercase() || first == '_' {
670 return ArgShape::Variable;
671 }
672 if first.is_ascii_lowercase() {
673 let bare = is_bare_identifier(s);
674 if bare {
675 return ArgShape::Atom {
676 name: s.to_string(),
677 };
678 }
679 if s.starts_with("fun ") || s == "fun" {
680 return ArgShape::Fun;
681 }
682 return ArgShape::Unknown;
683 }
684 ArgShape::Unknown
685}
686
687fn is_bare_identifier(s: &str) -> bool {
688 s.chars()
689 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
690}
691
692pub fn extract_dynamic_into(line: &str, out: &mut Vec<DynamicCall>) {
693 let line = strip_line_comment(line);
694 for _ in apply_bif_re().captures_iter(line) {
695 out.push(DynamicCall::Apply);
696 }
697 for _ in var_module_call_re().find_iter(line) {
698 out.push(DynamicCall::VariableDispatch);
699 }
700 for _ in var_function_call_re().find_iter(line) {
701 out.push(DynamicCall::VariableDispatch);
702 }
703 for caps in fun_ref_re().captures_iter(line) {
704 let variable_slot = |s: &str| s.starts_with(|c: char| c.is_ascii_uppercase() || c == '_');
705 let arity_raw = &caps[3];
706 let variable_arity = !arity_raw.bytes().all(|b| b.is_ascii_digit());
707 if variable_slot(&caps[1]) || variable_slot(&caps[2]) || variable_arity {
708 out.push(DynamicCall::VariableDispatch);
709 }
710 }
711}
712
713pub fn extract_definitions_into(text: &str, out: &mut Vec<SymbolRef>) {
714 for caps in fun_def_re().captures_iter(text) {
715 let head_end = caps.get(0).expect("capture").end();
716 let after = &text[head_end..];
717 let Ok(f) = FunctionName::from_str(&caps[1]) else {
718 continue;
719 };
720 let module = local_placeholder_module().clone();
721 match scan_arity(after) {
722 ScanArity::Exact(arity) => {
723 out.push(SymbolRef::function(Mfa::new(module, f, Arity::new(arity))));
724 }
725 ScanArity::Unterminated => {
726 out.push(SymbolRef::function_any_arity(module, f));
727 }
728 }
729 }
730}
731
732fn local_placeholder_module() -> &'static ModuleName {
733 static M: OnceLock<ModuleName> = OnceLock::new();
734 M.get_or_init(|| ModuleName::from_str("_local").expect("valid"))
735}
736
737#[derive(Debug, Default)]
740pub struct HunkScan {
741 pub referenced: Vec<SymbolRef>,
742 pub defined: Vec<SymbolRef>,
743 pub dynamic_calls: Vec<DynamicCall>,
744 pub call_args: Vec<(Mfa, Vec<ArgShape>)>,
745}
746
747pub fn scan_hunk(lines: &[(RefOrigin, &str)], macros: &MacroTable) -> HunkScan {
755 let mut out = HunkScan::default();
756 let mut scanner = AttrCtxScanner::new();
757 let mut text = String::new();
758 let mut line_origins: Vec<(usize, RefOrigin)> = Vec::new();
759 let mut run_ctx: Option<RefContext> = None;
760 for &(origin, raw) in lines {
761 let stripped = strip_line_comment(raw);
762 let ctx = scanner.classify(stripped);
763 if run_ctx != Some(ctx) {
764 if let Some(prev) = run_ctx {
765 flush_run(prev, &text, &line_origins, macros, &mut out);
766 }
767 text.clear();
768 line_origins.clear();
769 run_ctx = Some(ctx);
770 }
771 line_origins.push((text.len(), origin));
772 text.push_str(stripped);
773 text.push('\n');
774 }
775 if let Some(prev) = run_ctx {
776 flush_run(prev, &text, &line_origins, macros, &mut out);
777 }
778 out
779}
780
781fn flush_run(
782 ctx: RefContext,
783 text: &str,
784 line_origins: &[(usize, RefOrigin)],
785 macros: &MacroTable,
786 out: &mut HunkScan,
787) {
788 match ctx {
789 RefContext::Body => {
790 let mut refs = Vec::new();
791 push_refs_with_offsets(text, macros, &mut refs);
792 tag_by_origin(refs, line_origins, &mut out.referenced);
793 extract_definitions_into(text, &mut out.defined);
794 extract_dynamic_into(text, &mut out.dynamic_calls);
795 let mut args = Vec::new();
796 push_call_args_with_offsets(text, &mut args);
797 for (call, offset) in args {
800 if origin_at(line_origins, offset) == RefOrigin::Added {
801 out.call_args.push(call);
802 }
803 }
804 }
805 RefContext::TypeAttribute => {
806 let mut refs = Vec::new();
807 push_type_refs_with_offsets(text, &mut refs);
808 tag_by_origin(refs, line_origins, &mut out.referenced);
809 }
810 }
811}
812
813fn tag_by_origin(
814 refs: Vec<(SymbolRef, usize)>,
815 line_origins: &[(usize, RefOrigin)],
816 out: &mut Vec<SymbolRef>,
817) {
818 for (r, offset) in refs {
819 out.push(r.with_origin(origin_at(line_origins, offset)));
820 }
821}
822
823fn origin_at(line_origins: &[(usize, RefOrigin)], offset: usize) -> RefOrigin {
826 value_at_or_before(line_origins, offset, RefOrigin::Context)
827}