1use std::collections::{BTreeMap, BTreeSet};
22use std::path::{Path, PathBuf};
23use std::str;
24use std::str::FromStr;
25
26use backhopper_erlang_scan::{
27 arity_of_args, count_top_level_commas, dot_terminates, hash_inside_number, is_name_byte,
28 is_type_declaration_keyword, parse_callable_signature, quoted_atom_span,
29 skip_char_literal_span, string_span, take_balanced_parens,
30};
31
32use crate::compat::target_tree_index::TargetTreeIndex;
33use crate::compat::test_suite::module_resolves;
34use crate::model::names::{Arity, FunctionName, ModuleName, RelativePath, TypeName};
35use crate::model::symbol::RefContext;
36use crate::model::verdict::IncludeDirective;
37use crate::snapshot::spec_normalize::normalize_signature;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BehaviourRef {
43 pub behaviour: ModuleName,
44 pub line: u32,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct IncludeRef {
51 pub directive: IncludeDirective,
52 pub line: u32,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ScannedUse {
58 pub name: String,
59 pub line: u32,
60}
61
62pub fn extract_defined_macros(src: &str) -> BTreeSet<String> {
68 iter_attribute_bodies(src, &["define"])
69 .into_iter()
70 .filter_map(|hit| parse_macro_define_name(hit.body))
71 .collect()
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct MacroDef {
79 pub params: Option<usize>,
80 pub body: String,
81}
82
83pub fn extract_defined_macro_values(src: &str) -> BTreeMap<String, Vec<MacroDef>> {
88 let mut out: BTreeMap<String, Vec<MacroDef>> = BTreeMap::new();
89 for hit in iter_attribute_bodies(src, &["define"]) {
90 let Some((name, params, value)) = parse_macro_define(hit.body) else {
91 continue;
92 };
93 out.entry(name).or_default().push(MacroDef {
94 params,
95 body: normalize_macro_body(value),
96 });
97 }
98 out
99}
100
101fn parse_macro_define(body: &str) -> Option<(String, Option<usize>, &str)> {
103 let trimmed = body.trim_start();
104 let name_end = trimmed
105 .char_indices()
106 .find(|(_, c)| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '@'))
107 .map(|(i, _)| i)
108 .unwrap_or(trimmed.len());
109 if name_end == 0 {
110 return None;
111 }
112 let name = trimmed[..name_end].to_owned();
113 let rest = trimmed[name_end..].trim_start();
114 let (params, after) = if rest.starts_with('(') {
115 let (args, after_args) = take_balanced_parens(rest)?;
116 let count = if args.trim().is_empty() {
117 0
118 } else {
119 count_top_level_commas(args) + 1
120 };
121 (Some(count), after_args.trim_start())
122 } else {
123 (None, rest)
124 };
125 let value = after.strip_prefix(',')?;
126 Some((name, params, value))
127}
128
129fn normalize_macro_body(body: &str) -> String {
133 let mut out = String::with_capacity(body.len());
134 let mut chars = body.chars();
135 let mut in_string = false;
136 let mut in_atom = false;
137 while let Some(c) = chars.next() {
138 if in_string || in_atom {
139 out.push(c);
140 match c {
141 '\\' => {
142 if let Some(next) = chars.next() {
143 out.push(next);
144 }
145 }
146 '"' if in_string => in_string = false,
147 '\'' if in_atom => in_atom = false,
148 _ => {}
149 }
150 continue;
151 }
152 match c {
153 '%' => {
154 for next in chars.by_ref() {
155 if next == '\n' {
156 break;
157 }
158 }
159 if !out.ends_with(' ') {
160 out.push(' ');
161 }
162 }
163 '"' => {
164 in_string = true;
165 out.push(c);
166 }
167 '\'' => {
168 in_atom = true;
169 out.push(c);
170 }
171 '$' => {
172 out.push(c);
173 if let Some(next) = chars.next() {
174 out.push(next);
175 }
176 }
177 c if c.is_whitespace() => {
178 if !out.ends_with(' ') {
179 out.push(' ');
180 }
181 }
182 c => out.push(c),
183 }
184 }
185 out.trim().to_owned()
186}
187
188pub fn extract_macro_uses(src: &str) -> Vec<ScannedUse> {
191 scan_uses(src, b'?', |bytes, j| {
192 let mut k = j;
193 while k < bytes.len() && bytes[k] == b'?' {
194 k += 1;
195 }
196 let start = k;
197 while k < bytes.len() && is_name_byte(bytes[k]) {
198 k += 1;
199 }
200 (start, k)
201 })
202}
203
204pub fn extract_record_uses(src: &str) -> Vec<ScannedUse> {
208 scan_uses(src, b'#', |bytes, j| {
209 let mut start = j;
210 if bytes.get(start) == Some(&b'\'') {
211 start += 1;
212 let mut k = start;
213 while k < bytes.len() && bytes[k] != b'\'' {
214 k += 1;
215 }
216 return (start, k);
217 }
218 match bytes.get(start) {
221 Some(b) if b.is_ascii_lowercase() => {}
222 _ => return (start, start),
223 }
224 let mut k = start;
225 while k < bytes.len() && is_name_byte(bytes[k]) {
226 k += 1;
227 }
228 (start, k)
229 })
230}
231
232pub fn extract_defined_records(src: &str) -> BTreeSet<String> {
234 iter_attribute_bodies(src, &["record"])
235 .into_iter()
236 .filter_map(|hit| parse_single_atom_argument(hit.body))
237 .collect()
238}
239
240fn scan_uses(
244 src: &str,
245 sigil: u8,
246 name_span: impl Fn(&[u8], usize) -> (usize, usize),
247) -> Vec<ScannedUse> {
248 let bytes = src.as_bytes();
249 let mut out = Vec::new();
250 let mut i = 0;
251 let mut line = 1u32;
252 while i < bytes.len() {
253 match bytes[i] {
254 b'\n' => {
255 line += 1;
256 i += 1;
257 }
258 b'%' => {
259 while i < bytes.len() && bytes[i] != b'\n' {
260 i += 1;
261 }
262 }
263 b'"' | b'~' => {
264 let end = i + string_span(bytes, i).unwrap_or(1);
265 line += newline_count(&bytes[i..end]);
266 i = end;
267 }
268 b'$' => i += skip_char_literal_span(bytes, i),
269 b'\'' => {
270 let end = i + quoted_atom_span(bytes, i).unwrap_or(1);
271 line += newline_count(&bytes[i..end]);
272 i = end;
273 }
274 b'#' if sigil == b'#' && hash_inside_number(bytes, i) => i += 1,
275 b if b == sigil => {
276 let (start, end) = name_span(bytes, i + 1);
277 if end > start {
278 out.push(ScannedUse {
279 name: String::from_utf8_lossy(&bytes[start..end]).into_owned(),
280 line,
281 });
282 }
283 i = end.max(i + 1);
284 }
285 _ => i += 1,
286 }
287 }
288 out
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct FunctionSignature {
295 pub name: String,
296 pub arity: usize,
297 pub line: u32,
298 pub is_definition: bool,
299}
300
301pub fn extract_function_signatures(src: &str) -> Vec<FunctionSignature> {
307 signatures_with_line_context(src, None)
308}
309
310pub fn extract_function_signatures_with_context(
316 src: &str,
317 ctx: &[RefContext],
318) -> Vec<FunctionSignature> {
319 signatures_with_line_context(src, Some(ctx))
320}
321
322fn signatures_with_line_context(src: &str, ctx: Option<&[RefContext]>) -> Vec<FunctionSignature> {
323 let bytes = src.as_bytes();
324 let mut out = Vec::new();
325 let mut i = 0;
326 let mut line = 1u32;
327 let mut in_attribute = line_starts_attribute(ctx, line);
329 while i < bytes.len() {
330 match bytes[i] {
331 b'\n' => {
332 line += 1;
333 if ctx.is_some() {
334 in_attribute = line_starts_attribute(ctx, line);
335 }
336 i += 1;
337 }
338 b'%' => {
339 while i < bytes.len() && bytes[i] != b'\n' {
340 i += 1;
341 }
342 }
343 b'"' | b'~' => {
344 let end = i + string_span(bytes, i).unwrap_or(1);
345 line += count_newlines(&bytes[i..end]);
346 i = end;
347 }
348 b'$' => i += skip_char_literal_span(bytes, i),
349 b'\'' => {
350 let end = i + quoted_atom_span(bytes, i).unwrap_or(1);
351 line += count_newlines(&bytes[i..end]);
352 i = end;
353 }
354 b'-' if !in_attribute && at_form_start(bytes, i) => {
355 in_attribute = true;
356 i += 1;
357 }
358 b'.' if in_attribute && dot_terminates(bytes, i) => {
359 in_attribute = false;
360 i += 1;
361 }
362 b if b.is_ascii_uppercase() || b == b'_' => {
363 while i < bytes.len() && is_name_byte(bytes[i]) {
365 i += 1;
366 }
367 }
368 b if b.is_ascii_lowercase() => {
369 let start = i;
370 let mut j = i;
371 while j < bytes.len() && is_name_byte(bytes[j]) {
372 j += 1;
373 }
374 let name = &bytes[start..j];
375 let qualified = start > 0 && matches!(bytes[start - 1], b':' | b'?' | b'#');
376 if bytes.get(j) == Some(&b'(') && !qualified && !is_reserved_word(name) {
377 if let Some(close) = match_parens(bytes, j + 1) {
378 if !in_attribute {
379 let arity = arity_of_args(&src[j + 1..close]);
380 let is_definition = followed_by_clause_arrow(bytes, close + 1)
383 || starts_line(bytes, start);
384 out.push(FunctionSignature {
385 name: String::from_utf8_lossy(name).into_owned(),
386 arity,
387 line,
388 is_definition,
389 });
390 }
391 line += count_newlines(&bytes[start..=close]);
392 i = close + 1;
393 continue;
394 }
395 }
396 i = j;
397 }
398 _ => i += 1,
399 }
400 }
401 out
402}
403
404fn line_starts_attribute(ctx: Option<&[RefContext]>, line: u32) -> bool {
407 ctx.is_some_and(|ctx| {
408 ctx.get(line.saturating_sub(1) as usize)
409 .is_some_and(|c| c.is_attribute())
410 })
411}
412
413fn starts_line(bytes: &[u8], start: usize) -> bool {
415 start == 0 || bytes[start - 1] == b'\n'
416}
417
418fn at_form_start(bytes: &[u8], pos: usize) -> bool {
421 bytes[..pos]
422 .iter()
423 .rev()
424 .take_while(|&&b| b != b'\n')
425 .all(|&b| b.is_ascii_whitespace())
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
432pub struct ImportedFunction {
433 pub module: ModuleName,
434 pub function: FunctionName,
435 pub arity: Arity,
436}
437
438pub fn extract_imports(src: &str) -> BTreeSet<ImportedFunction> {
443 let mut out = BTreeSet::new();
444 for hit in iter_attribute_bodies(src, &["import"]) {
445 let Some(open) = hit.body.find('[') else {
446 continue;
447 };
448 let Some(close) = hit.body[open..].find(']') else {
449 continue;
450 };
451 let module_text = hit.body[..open].trim().trim_end_matches(',').trim();
452 let Ok(module) = ModuleName::from_str(module_text) else {
453 continue;
454 };
455 for entry in hit.body[open + 1..open + close].split(',') {
456 if let Some((name, arity)) = parse_fun_arity(entry)
457 && let (Ok(function), Ok(arity)) =
458 (FunctionName::from_str(&name), Arity::try_from(arity))
459 {
460 out.insert(ImportedFunction {
461 module: module.clone(),
462 function,
463 arity,
464 });
465 }
466 }
467 }
468 out
469}
470
471#[must_use]
475pub fn declares_parse_transform(src: &str) -> bool {
476 iter_attribute_bodies(src, &["compile"])
477 .iter()
478 .any(|hit| hit.body.contains("parse_transform"))
479}
480
481#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ExportSet {
485 pub exports: BTreeSet<(FunctionName, Arity)>,
486 pub complete: bool,
487}
488
489pub fn extract_exports(src: &str) -> ExportSet {
497 let mut exports = BTreeSet::new();
498 let mut complete = true;
499 for hit in iter_attribute_bodies(src, &["compile"]) {
500 if hit.body.contains("export_all") || hit.body.contains("parse_transform") {
501 complete = false;
502 }
503 }
504 for hit in iter_attribute_bodies(src, &["export"]) {
505 let Some(open) = hit.body.find('[') else {
506 continue;
507 };
508 let Some(close) = hit.body[open..].find(']') else {
509 continue;
510 };
511 for entry in hit.body[open + 1..open + close].split(',') {
512 let entry = entry.trim();
513 if entry.is_empty() {
514 continue;
515 }
516 if entry.contains('?') {
518 complete = false;
519 continue;
520 }
521 if let Some((name, arity)) = parse_fun_arity(entry)
522 && let (Ok(function), Ok(arity)) =
523 (FunctionName::from_str(&name), Arity::try_from(arity))
524 {
525 exports.insert((function, arity));
526 }
527 }
528 }
529 ExportSet { exports, complete }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct ExportedType {
535 pub name: TypeName,
536 pub arity: Arity,
537 pub line: u32,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq)]
546pub struct ExportedTypeSet {
547 pub types: Vec<ExportedType>,
548 pub complete: bool,
549}
550
551pub fn extract_exported_types(src: &str) -> ExportedTypeSet {
555 let mut types = Vec::new();
556 let mut complete = !declares_parse_transform(src);
557 for hit in iter_attribute_bodies(src, &["export_type"]) {
558 let Some(open) = hit.body.find('[') else {
559 continue;
560 };
561 let Some(close) = hit.body[open..].find(']') else {
562 continue;
563 };
564 for entry in hit.body[open + 1..open + close].split(',') {
565 let entry = entry.trim();
566 if entry.is_empty() {
567 continue;
568 }
569 if entry.contains('?') {
571 complete = false;
572 continue;
573 }
574 if let Some((name, arity)) = parse_fun_arity(entry)
575 && let (Ok(name), Ok(arity)) = (TypeName::from_str(&name), Arity::try_from(arity))
576 {
577 types.push(ExportedType {
578 name,
579 arity,
580 line: hit.line,
581 });
582 }
583 }
584 }
585 ExportedTypeSet { types, complete }
586}
587
588pub fn has_macro_expanded_attribute(src: &str) -> bool {
593 let bytes = src.as_bytes();
594 let mut i = 0;
595 while i < bytes.len() {
596 match bytes[i] {
597 b'%' => {
598 while i < bytes.len() && bytes[i] != b'\n' {
599 i += 1;
600 }
601 }
602 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
603 b'$' => i += skip_char_literal_span(bytes, i),
604 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
605 b'-' if at_form_start(bytes, i) => {
606 let rest = &bytes[i + 1..];
607 let after_space = rest.iter().position(|b| !b.is_ascii_whitespace());
608 if after_space.is_some_and(|p| rest[p] == b'?') {
609 return true;
610 }
611 i += 1;
612 }
613 _ => i += 1,
614 }
615 }
616 false
617}
618
619pub fn extract_defined_types(src: &str) -> BTreeSet<(TypeName, Arity)> {
623 let bytes = src.as_bytes();
624 let mut out = BTreeSet::new();
625 let mut i = 0;
626 while i < bytes.len() {
627 match bytes[i] {
628 b'%' => {
629 while i < bytes.len() && bytes[i] != b'\n' {
630 i += 1;
631 }
632 }
633 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
634 b'$' => i += skip_char_literal_span(bytes, i),
635 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
636 b'-' if at_form_start(bytes, i) => {
637 let name_start = i + 1;
638 let mut name_end = name_start;
639 while name_end < bytes.len() && is_attribute_name_char(bytes[name_end]) {
640 name_end += 1;
641 }
642 let name = str::from_utf8(&bytes[name_start..name_end]).unwrap_or("");
643 if !is_type_declaration_keyword(name) {
644 i += 1;
645 continue;
646 }
647 let body_start = name_end;
648 let body_end = form_end(bytes, body_start);
649 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
650 if let Some((name, arity)) = parse_type_name_arity(strip_outer_parens(body))
651 && let (Ok(name), Ok(arity)) =
652 (TypeName::from_str(&name), Arity::try_from(arity))
653 {
654 out.insert((name, arity));
655 }
656 i = body_end;
657 }
658 _ => i += 1,
659 }
660 }
661 out
662}
663
664fn parse_type_name_arity(body: &str) -> Option<(String, usize)> {
667 let trimmed = body.trim();
668 let name_end = trimmed
669 .bytes()
670 .position(|b| !is_name_byte(b))
671 .unwrap_or(trimmed.len());
672 if name_end == 0 {
673 return None;
674 }
675 let name = &trimmed[..name_end];
676 let (args, rest) = take_balanced_parens(trimmed[name_end..].trim_start())?;
677 rest.trim_start().strip_prefix("::")?;
678 let arity = if args.trim().is_empty() {
679 0
680 } else {
681 count_top_level_commas(args) + 1
682 };
683 Some((name.to_owned(), arity))
684}
685
686pub type SpecTable = BTreeMap<(FunctionName, Arity), String>;
688
689pub fn extract_specs(src: &str) -> SpecTable {
696 extract_signature_forms(src, b"spec")
697}
698
699pub fn extract_callbacks(src: &str) -> SpecTable {
703 extract_signature_forms(src, b"callback")
704}
705
706pub fn extract_optional_callbacks(src: &str) -> BTreeSet<(FunctionName, Arity)> {
709 let mut out = BTreeSet::new();
710 for hit in iter_attribute_bodies(src, &["optional_callbacks"]) {
711 let Some(open) = hit.body.find('[') else {
712 continue;
713 };
714 let Some(close) = hit.body[open..].find(']') else {
715 continue;
716 };
717 for entry in hit.body[open + 1..open + close].split(',') {
718 if let Some((name, arity)) = parse_fun_arity(entry)
719 && let (Ok(function), Ok(arity)) =
720 (FunctionName::from_str(&name), Arity::try_from(arity))
721 {
722 out.insert((function, arity));
723 }
724 }
725 }
726 out
727}
728
729fn extract_signature_forms(src: &str, keyword: &[u8]) -> SpecTable {
730 let bytes = src.as_bytes();
731 let mut out = SpecTable::new();
732 let mut i = 0;
733 while i < bytes.len() {
734 match bytes[i] {
735 b'%' => {
736 while i < bytes.len() && bytes[i] != b'\n' {
737 i += 1;
738 }
739 }
740 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
741 b'$' => i += skip_char_literal_span(bytes, i),
742 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
743 b'-' if at_form_start(bytes, i) && at_keyword(bytes, i + 1, keyword) => {
744 let body_start = i + 1 + keyword.len();
745 let body_end = form_end(bytes, body_start);
746 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
747 if let Some(sig) = parse_callable_signature(strip_outer_parens(body))
748 && let Ok(function) = FunctionName::from_str(&sig.name)
749 {
750 out.insert(
751 (function, Arity::new(sig.arity)),
752 normalize_signature(&sig.signature),
753 );
754 }
755 i = body_end;
756 }
757 _ => i += 1,
758 }
759 }
760 out
761}
762
763fn at_keyword(bytes: &[u8], pos: usize, keyword: &[u8]) -> bool {
766 bytes[pos..].starts_with(keyword)
767 && !bytes
768 .get(pos + keyword.len())
769 .copied()
770 .is_some_and(is_attribute_name_char)
771}
772
773fn strip_outer_parens(body: &str) -> &str {
776 let trimmed = body.trim();
777 match trimmed.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
778 Some(inner) => inner.trim(),
779 None => trimmed,
780 }
781}
782
783fn form_end(bytes: &[u8], start: usize) -> usize {
787 let mut i = start;
788 while i < bytes.len() {
789 match bytes[i] {
790 b'%' => {
791 while i < bytes.len() && bytes[i] != b'\n' {
792 i += 1;
793 }
794 }
795 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
796 b'$' => i += skip_char_literal_span(bytes, i),
797 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
798 b'.' if dot_terminates(bytes, i) => return i,
799 _ => i += 1,
800 }
801 }
802 i
803}
804
805fn parse_fun_arity(entry: &str) -> Option<(String, usize)> {
806 let (name, arity) = entry.trim().split_once('/')?;
807 let name = name.trim();
808 if name.is_empty() || !name.bytes().all(is_name_byte) {
809 return None;
810 }
811 Some((name.to_owned(), arity.trim().parse().ok()?))
812}
813
814fn is_reserved_word(name: &[u8]) -> bool {
815 matches!(
816 name,
817 b"after"
818 | b"and"
819 | b"andalso"
820 | b"band"
821 | b"begin"
822 | b"bnot"
823 | b"bor"
824 | b"bsl"
825 | b"bsr"
826 | b"bxor"
827 | b"case"
828 | b"catch"
829 | b"cond"
830 | b"div"
831 | b"end"
832 | b"fun"
833 | b"if"
834 | b"let"
835 | b"maybe"
836 | b"not"
837 | b"of"
838 | b"or"
839 | b"orelse"
840 | b"receive"
841 | b"rem"
842 | b"try"
843 | b"when"
844 | b"xor"
845 )
846}
847
848fn followed_by_clause_arrow(bytes: &[u8], pos: usize) -> bool {
851 let mut i = pos;
852 while i < bytes.len() && (bytes[i] as char).is_whitespace() {
853 i += 1;
854 }
855 bytes[i..].starts_with(b"->") || bytes[i..].starts_with(b"when")
856}
857
858fn count_newlines(bytes: &[u8]) -> u32 {
859 let mut n = 0u32;
860 for &b in bytes {
861 if b == b'\n' {
862 n += 1;
863 }
864 }
865 n
866}
867
868#[must_use]
870pub fn is_predefined_macro(name: &str) -> bool {
871 matches!(
872 name,
873 "MODULE"
874 | "MODULE_STRING"
875 | "FILE"
876 | "LINE"
877 | "MACHINE"
878 | "FUNCTION_NAME"
879 | "FUNCTION_ARITY"
880 | "OTP_RELEASE"
881 | "FEATURE_AVAILABLE"
882 | "FEATURE_ENABLED"
883 )
884}
885
886fn parse_macro_define_name(body: &str) -> Option<String> {
887 let trimmed = body.trim_start();
888 let mut chars = trimmed.char_indices();
889 let (_, first) = chars.next()?;
890 if first == '\'' {
891 let end = trimmed[1..].find('\'')?;
892 return Some(trimmed[1..=end].to_owned());
893 }
894 if !(first.is_ascii_alphanumeric() || first == '_' || first == '@') {
895 return None;
896 }
897 let end = trimmed
898 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '@'))
899 .unwrap_or(trimmed.len());
900 Some(trimmed[..end].to_owned())
901}
902
903pub fn extract_behaviours(src: &str) -> Vec<BehaviourRef> {
907 extract_attribute_atom_argument(src, &["behaviour", "behavior"])
908 .into_iter()
909 .filter_map(|(atom, line)| {
910 ModuleName::new(atom)
911 .ok()
912 .map(|m| BehaviourRef { behaviour: m, line })
913 })
914 .collect()
915}
916
917pub fn extract_includes(src: &str) -> Vec<IncludeRef> {
921 let mut out = Vec::new();
922 for (name, path, line) in extract_attribute_string_argument(src, &["include", "include_lib"]) {
923 let directive = match name.as_str() {
924 "include" => IncludeDirective::Include { path },
925 "include_lib" => IncludeDirective::IncludeLib { path },
926 _ => continue,
927 };
928 out.push(IncludeRef { directive, line });
929 }
930 out
931}
932
933pub fn behaviour_resolves(
936 target: &TargetTreeIndex,
937 search_path_globs: &[String],
938 behaviour: &ModuleName,
939) -> bool {
940 module_resolves(target, search_path_globs, behaviour)
941}
942
943pub fn resolve_include(
951 target: &TargetTreeIndex,
952 source_path: &RelativePath,
953 directive: &IncludeDirective,
954) -> Result<RelativePath, Vec<RelativePath>> {
955 let candidates = include_candidates(source_path, directive);
956 for c in &candidates {
957 if target.contains_path(Path::new(c.as_str())) {
958 return Ok(c.clone());
959 }
960 }
961 Err(candidates)
962}
963
964fn include_candidates(
967 source_path: &RelativePath,
968 directive: &IncludeDirective,
969) -> Vec<RelativePath> {
970 match directive {
971 IncludeDirective::Include { path } => {
972 let source_dir = Path::new(source_path.as_str())
973 .parent()
974 .map(Path::to_path_buf)
975 .unwrap_or_default();
976 let joined = source_dir.join(path);
977 normalised_relative_path(&joined).into_iter().collect()
978 }
979 IncludeDirective::IncludeLib { path } => include_lib_candidates(path),
980 }
981}
982
983fn include_lib_candidates(path: &str) -> Vec<RelativePath> {
989 let mut segs = path.splitn(2, '/');
990 let app = match segs.next() {
991 Some(s) if !s.is_empty() => s,
992 _ => return Vec::new(),
993 };
994 let rest = match segs.next() {
995 Some(s) => s,
996 None => return Vec::new(),
997 };
998 let mut out = Vec::new();
999 for prefix in ["deps", "apps", "lib"] {
1000 let candidate = PathBuf::from(prefix).join(app).join(rest);
1001 if let Some(rp) = normalised_relative_path(&candidate) {
1002 out.push(rp);
1003 }
1004 }
1005 out
1006}
1007
1008fn normalised_relative_path(p: &Path) -> Option<RelativePath> {
1009 let s = p.to_string_lossy().replace('\\', "/");
1010 RelativePath::new(s).ok()
1011}
1012
1013fn extract_attribute_atom_argument(src: &str, names: &[&str]) -> Vec<(String, u32)> {
1014 let mut out = Vec::new();
1015 for hit in iter_attribute_bodies(src, names) {
1016 if let Some(atom) = parse_single_atom_argument(hit.body) {
1017 out.push((atom, hit.line));
1018 }
1019 }
1020 out
1021}
1022
1023fn extract_attribute_string_argument(src: &str, names: &[&str]) -> Vec<(String, String, u32)> {
1024 let mut out = Vec::new();
1025 for hit in iter_attribute_bodies(src, names) {
1026 if let Some(path) = parse_single_string_argument(hit.body) {
1027 out.push((hit.name.to_owned(), path, hit.line));
1028 }
1029 }
1030 out
1031}
1032
1033#[derive(Debug, Clone, Copy)]
1034struct AttrHit<'a> {
1035 name: &'a str,
1036 body: &'a str,
1037 line: u32,
1038}
1039
1040fn iter_attribute_bodies<'a>(src: &'a str, names: &'a [&'a str]) -> Vec<AttrHit<'a>> {
1044 let bytes = src.as_bytes();
1045 let mut out = Vec::new();
1046 let mut i = 0;
1047 let mut line = 1u32;
1048 while i < bytes.len() {
1049 let b = bytes[i];
1050 if b == b'\n' {
1051 line += 1;
1052 i += 1;
1053 continue;
1054 }
1055 if b == b'%' {
1056 while i < bytes.len() && bytes[i] != b'\n' {
1057 i += 1;
1058 }
1059 continue;
1060 }
1061 if b == b'"' || b == b'~' {
1062 let end = i + string_span(bytes, i).unwrap_or(1);
1063 line += newline_count(&bytes[i..end]);
1064 i = end;
1065 continue;
1066 }
1067 if b == b'$' {
1068 i += skip_char_literal_span(bytes, i);
1069 continue;
1070 }
1071 if b == b'\'' {
1072 let end = i + quoted_atom_span(bytes, i).unwrap_or(1);
1073 line += newline_count(&bytes[i..end]);
1074 i = end;
1075 continue;
1076 }
1077 if b == b'-'
1078 && let Some(name) = match_attribute_keyword(bytes, i + 1, names)
1079 {
1080 let name_end = i + 1 + name.len();
1081 let mut j = name_end;
1082 while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
1083 j += 1;
1084 }
1085 if j >= bytes.len() || bytes[j] != b'(' {
1086 i = name_end;
1087 continue;
1088 }
1089 let body_start = j + 1;
1090 if let Some(body_end) = match_parens(bytes, body_start) {
1091 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
1092 out.push(AttrHit { name, body, line });
1093 line += newline_count(&bytes[i..=body_end]);
1094 i = body_end + 1;
1095 continue;
1096 }
1097 i = name_end;
1098 continue;
1099 }
1100 i += 1;
1101 }
1102 out
1103}
1104
1105fn match_attribute_keyword<'a>(
1106 bytes: &[u8],
1107 start: usize,
1108 names: &'a [&'a str],
1109) -> Option<&'a str> {
1110 for name in names {
1111 let nb = name.as_bytes();
1112 if start + nb.len() > bytes.len() {
1113 continue;
1114 }
1115 if &bytes[start..start + nb.len()] != nb {
1116 continue;
1117 }
1118 let after = start + nb.len();
1119 let next = bytes.get(after).copied().unwrap_or(b' ');
1120 if !is_attribute_name_char(next) {
1121 return Some(name);
1122 }
1123 }
1124 None
1125}
1126
1127fn is_attribute_name_char(b: u8) -> bool {
1128 b.is_ascii_alphanumeric() || b == b'_'
1129}
1130
1131fn match_parens(bytes: &[u8], start: usize) -> Option<usize> {
1132 let mut depth = 1i32;
1133 let mut i = start;
1134 while i < bytes.len() && depth > 0 {
1135 let b = bytes[i];
1136 match b {
1137 b'(' => depth += 1,
1138 b')' => {
1139 depth -= 1;
1140 if depth == 0 {
1141 return Some(i);
1142 }
1143 }
1144 b'"' | b'~' => {
1145 i += string_span(bytes, i).unwrap_or(1);
1146 continue;
1147 }
1148 b'$' => {
1149 i += skip_char_literal_span(bytes, i);
1150 continue;
1151 }
1152 b'\'' => {
1153 i += quoted_atom_span(bytes, i).unwrap_or(1);
1154 continue;
1155 }
1156 b'%' => {
1157 while i < bytes.len() && bytes[i] != b'\n' {
1158 i += 1;
1159 }
1160 continue;
1161 }
1162 _ => {}
1163 }
1164 i += 1;
1165 }
1166 None
1167}
1168
1169fn newline_count(span: &[u8]) -> u32 {
1170 let mut n = 0;
1171 for &b in span {
1172 if b == b'\n' {
1173 n += 1;
1174 }
1175 }
1176 n
1177}
1178
1179fn parse_single_atom_argument(body: &str) -> Option<String> {
1180 let trimmed = body.trim();
1181 if trimmed.is_empty() {
1182 return None;
1183 }
1184 let mut chars = trimmed.chars();
1185 let first = chars.next()?;
1186 if first == '\'' {
1187 let end = trimmed[1..].find('\'')?;
1188 return Some(trimmed[1..=end].to_owned());
1189 }
1190 if !first.is_ascii_lowercase() {
1191 return None;
1192 }
1193 let mut end = 1;
1194 for c in chars {
1195 if c.is_ascii_alphanumeric() || c == '_' || c == '@' {
1196 end += c.len_utf8();
1197 continue;
1198 }
1199 break;
1200 }
1201 Some(trimmed[..end].to_owned())
1202}
1203
1204fn parse_single_string_argument(body: &str) -> Option<String> {
1205 let trimmed = body.trim();
1206 let first = trimmed.chars().next()?;
1207 if first != '"' {
1208 return None;
1209 }
1210 let bytes = trimmed.as_bytes();
1211 let mut i = 1usize;
1212 let mut out = String::new();
1213 while i < bytes.len() {
1214 let b = bytes[i];
1215 if b == b'\\' && i + 1 < bytes.len() {
1216 out.push(bytes[i + 1] as char);
1217 i += 2;
1218 continue;
1219 }
1220 if b == b'"' {
1221 return Some(out);
1222 }
1223 out.push(b as char);
1224 i += 1;
1225 }
1226 None
1227}