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