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, parse_callable_signature, take_balanced_parens,
28};
29
30use crate::compat::target_tree_index::TargetTreeIndex;
31use crate::compat::test_suite::module_resolves;
32use crate::model::names::{Arity, FunctionName, ModuleName, RelativePath, TypeName};
33use crate::model::symbol::RefContext;
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 signatures_with_line_context(src, None)
299}
300
301pub fn extract_function_signatures_with_context(
307 src: &str,
308 ctx: &[RefContext],
309) -> Vec<FunctionSignature> {
310 signatures_with_line_context(src, Some(ctx))
311}
312
313fn signatures_with_line_context(src: &str, ctx: Option<&[RefContext]>) -> Vec<FunctionSignature> {
314 let bytes = src.as_bytes();
315 let mut out = Vec::new();
316 let mut i = 0;
317 let mut line = 1u32;
318 let mut in_attribute = line_starts_attribute(ctx, line);
320 while i < bytes.len() {
321 match bytes[i] {
322 b'\n' => {
323 line += 1;
324 if ctx.is_some() {
325 in_attribute = line_starts_attribute(ctx, line);
326 }
327 i += 1;
328 }
329 b'%' => {
330 while i < bytes.len() && bytes[i] != b'\n' {
331 i += 1;
332 }
333 }
334 b'"' => {
335 let end = skip_string(bytes, i, b'"');
336 line += count_newlines(&bytes[i..end]);
337 i = end;
338 }
339 b'$' => i += 2.min(bytes.len() - i),
340 b'\'' => {
341 let end = skip_string(bytes, i, b'\'');
342 line += count_newlines(&bytes[i..end]);
343 i = end;
344 }
345 b'-' if !in_attribute && at_form_start(bytes, i) => {
346 in_attribute = true;
347 i += 1;
348 }
349 b'.' if in_attribute && is_form_terminator(bytes, i) => {
350 in_attribute = false;
351 i += 1;
352 }
353 b if b.is_ascii_uppercase() || b == b'_' => {
354 while i < bytes.len() && is_name_char(bytes[i]) {
356 i += 1;
357 }
358 }
359 b if b.is_ascii_lowercase() => {
360 let start = i;
361 let mut j = i;
362 while j < bytes.len() && is_name_char(bytes[j]) {
363 j += 1;
364 }
365 let name = &bytes[start..j];
366 let qualified = start > 0 && matches!(bytes[start - 1], b':' | b'?' | b'#');
367 if bytes.get(j) == Some(&b'(') && !qualified && !is_reserved_word(name) {
368 if let Some(close) = match_parens(bytes, j + 1) {
369 if !in_attribute {
370 let arity = arity_of_args(&src[j + 1..close]);
371 let is_definition = followed_by_clause_arrow(bytes, close + 1)
374 || starts_line(bytes, start);
375 out.push(FunctionSignature {
376 name: String::from_utf8_lossy(name).into_owned(),
377 arity,
378 line,
379 is_definition,
380 });
381 }
382 line += count_newlines(&bytes[start..=close]);
383 i = close + 1;
384 continue;
385 }
386 }
387 i = j;
388 }
389 _ => i += 1,
390 }
391 }
392 out
393}
394
395fn line_starts_attribute(ctx: Option<&[RefContext]>, line: u32) -> bool {
398 ctx.is_some_and(|ctx| {
399 ctx.get(line.saturating_sub(1) as usize)
400 .is_some_and(|c| c.is_attribute())
401 })
402}
403
404fn starts_line(bytes: &[u8], start: usize) -> bool {
406 start == 0 || bytes[start - 1] == b'\n'
407}
408
409fn at_form_start(bytes: &[u8], pos: usize) -> bool {
412 bytes[..pos]
413 .iter()
414 .rev()
415 .take_while(|&&b| b != b'\n')
416 .all(|&b| b.is_ascii_whitespace())
417}
418
419fn is_form_terminator(bytes: &[u8], pos: usize) -> bool {
422 match bytes.get(pos + 1) {
423 None => true,
424 Some(b) => b.is_ascii_whitespace(),
425 }
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
588const TYPE_DECL_KEYWORDS: [&[u8]; 3] = [b"type", b"opaque", b"nominal"];
591
592pub fn has_macro_expanded_attribute(src: &str) -> bool {
597 let bytes = src.as_bytes();
598 let mut i = 0;
599 while i < bytes.len() {
600 match bytes[i] {
601 b'%' => {
602 while i < bytes.len() && bytes[i] != b'\n' {
603 i += 1;
604 }
605 }
606 b'"' => i = skip_string(bytes, i, b'"'),
607 b'$' => i += 2.min(bytes.len() - i),
608 b'\'' => i = skip_string(bytes, i, b'\''),
609 b'-' if at_form_start(bytes, i) => {
610 let rest = &bytes[i + 1..];
611 let after_space = rest.iter().position(|b| !b.is_ascii_whitespace());
612 if after_space.is_some_and(|p| rest[p] == b'?') {
613 return true;
614 }
615 i += 1;
616 }
617 _ => i += 1,
618 }
619 }
620 false
621}
622
623pub fn extract_defined_types(src: &str) -> BTreeSet<(TypeName, Arity)> {
627 let bytes = src.as_bytes();
628 let mut out = BTreeSet::new();
629 let mut i = 0;
630 while i < bytes.len() {
631 match bytes[i] {
632 b'%' => {
633 while i < bytes.len() && bytes[i] != b'\n' {
634 i += 1;
635 }
636 }
637 b'"' => i = skip_string(bytes, i, b'"'),
638 b'$' => i += 2.min(bytes.len() - i),
639 b'\'' => i = skip_string(bytes, i, b'\''),
640 b'-' if at_form_start(bytes, i) => {
641 let Some(keyword) = TYPE_DECL_KEYWORDS
642 .iter()
643 .find(|k| at_keyword(bytes, i + 1, k))
644 else {
645 i += 1;
646 continue;
647 };
648 let body_start = i + 1 + keyword.len();
649 let body_end = form_end(bytes, body_start);
650 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
651 if let Some((name, arity)) = parse_type_name_arity(strip_outer_parens(body))
652 && let (Ok(name), Ok(arity)) =
653 (TypeName::from_str(&name), Arity::try_from(arity))
654 {
655 out.insert((name, arity));
656 }
657 i = body_end;
658 }
659 _ => i += 1,
660 }
661 }
662 out
663}
664
665fn parse_type_name_arity(body: &str) -> Option<(String, usize)> {
668 let trimmed = body.trim();
669 let name_end = trimmed
670 .bytes()
671 .position(|b| !is_name_char(b))
672 .unwrap_or(trimmed.len());
673 if name_end == 0 {
674 return None;
675 }
676 let name = &trimmed[..name_end];
677 let (args, rest) = take_balanced_parens(trimmed[name_end..].trim_start())?;
678 rest.trim_start().strip_prefix("::")?;
679 let arity = if args.trim().is_empty() {
680 0
681 } else {
682 count_top_level_commas(args) + 1
683 };
684 Some((name.to_owned(), arity))
685}
686
687pub type SpecTable = BTreeMap<(FunctionName, Arity), String>;
689
690pub fn extract_specs(src: &str) -> SpecTable {
697 extract_signature_forms(src, b"spec")
698}
699
700pub fn extract_callbacks(src: &str) -> SpecTable {
704 extract_signature_forms(src, b"callback")
705}
706
707pub fn extract_optional_callbacks(src: &str) -> BTreeSet<(FunctionName, Arity)> {
710 let mut out = BTreeSet::new();
711 for hit in iter_attribute_bodies(src, &["optional_callbacks"]) {
712 let Some(open) = hit.body.find('[') else {
713 continue;
714 };
715 let Some(close) = hit.body[open..].find(']') else {
716 continue;
717 };
718 for entry in hit.body[open + 1..open + close].split(',') {
719 if let Some((name, arity)) = parse_fun_arity(entry)
720 && let (Ok(function), Ok(arity)) =
721 (FunctionName::from_str(&name), Arity::try_from(arity))
722 {
723 out.insert((function, arity));
724 }
725 }
726 }
727 out
728}
729
730fn extract_signature_forms(src: &str, keyword: &[u8]) -> SpecTable {
731 let bytes = src.as_bytes();
732 let mut out = SpecTable::new();
733 let mut i = 0;
734 while i < bytes.len() {
735 match bytes[i] {
736 b'%' => {
737 while i < bytes.len() && bytes[i] != b'\n' {
738 i += 1;
739 }
740 }
741 b'"' => i = skip_string(bytes, i, b'"'),
742 b'$' => i += 2.min(bytes.len() - i),
743 b'\'' => i = skip_string(bytes, i, b'\''),
744 b'-' if at_form_start(bytes, i) && at_keyword(bytes, i + 1, keyword) => {
745 let body_start = i + 1 + keyword.len();
746 let body_end = form_end(bytes, body_start);
747 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
748 if let Some(sig) = parse_callable_signature(strip_outer_parens(body))
749 && let Ok(function) = FunctionName::from_str(&sig.name)
750 {
751 out.insert(
752 (function, Arity::new(sig.arity)),
753 normalize_signature(&sig.signature),
754 );
755 }
756 i = body_end;
757 }
758 _ => i += 1,
759 }
760 }
761 out
762}
763
764fn at_keyword(bytes: &[u8], pos: usize, keyword: &[u8]) -> bool {
767 bytes[pos..].starts_with(keyword)
768 && !bytes
769 .get(pos + keyword.len())
770 .copied()
771 .is_some_and(is_attribute_name_char)
772}
773
774fn strip_outer_parens(body: &str) -> &str {
777 let trimmed = body.trim();
778 match trimmed.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
779 Some(inner) => inner.trim(),
780 None => trimmed,
781 }
782}
783
784fn form_end(bytes: &[u8], start: usize) -> usize {
788 let mut i = start;
789 while i < bytes.len() {
790 match bytes[i] {
791 b'%' => {
792 while i < bytes.len() && bytes[i] != b'\n' {
793 i += 1;
794 }
795 }
796 b'"' => i = skip_string(bytes, i, b'"'),
797 b'$' => i += 2.min(bytes.len() - i),
798 b'\'' => i = skip_string(bytes, i, b'\''),
799 b'.' if is_form_terminator(bytes, i) => return i,
800 _ => i += 1,
801 }
802 }
803 i
804}
805
806fn parse_fun_arity(entry: &str) -> Option<(String, usize)> {
807 let (name, arity) = entry.trim().split_once('/')?;
808 let name = name.trim();
809 if name.is_empty() || !name.bytes().all(is_name_char) {
810 return None;
811 }
812 Some((name.to_owned(), arity.trim().parse().ok()?))
813}
814
815fn is_reserved_word(name: &[u8]) -> bool {
816 matches!(
817 name,
818 b"after"
819 | b"and"
820 | b"andalso"
821 | b"band"
822 | b"begin"
823 | b"bnot"
824 | b"bor"
825 | b"bsl"
826 | b"bsr"
827 | b"bxor"
828 | b"case"
829 | b"catch"
830 | b"cond"
831 | b"div"
832 | b"end"
833 | b"fun"
834 | b"if"
835 | b"let"
836 | b"maybe"
837 | b"not"
838 | b"of"
839 | b"or"
840 | b"orelse"
841 | b"receive"
842 | b"rem"
843 | b"try"
844 | b"when"
845 | b"xor"
846 )
847}
848
849fn followed_by_clause_arrow(bytes: &[u8], pos: usize) -> bool {
852 let mut i = pos;
853 while i < bytes.len() && (bytes[i] as char).is_whitespace() {
854 i += 1;
855 }
856 bytes[i..].starts_with(b"->") || bytes[i..].starts_with(b"when")
857}
858
859fn count_newlines(bytes: &[u8]) -> u32 {
860 let mut n = 0u32;
861 for &b in bytes {
862 if b == b'\n' {
863 n += 1;
864 }
865 }
866 n
867}
868
869#[must_use]
871pub fn is_predefined_macro(name: &str) -> bool {
872 matches!(
873 name,
874 "MODULE"
875 | "MODULE_STRING"
876 | "FILE"
877 | "LINE"
878 | "MACHINE"
879 | "FUNCTION_NAME"
880 | "FUNCTION_ARITY"
881 | "OTP_RELEASE"
882 | "FEATURE_AVAILABLE"
883 | "FEATURE_ENABLED"
884 )
885}
886
887fn is_name_char(b: u8) -> bool {
888 b.is_ascii_alphanumeric() || b == b'_' || b == b'@'
889}
890
891fn parse_macro_define_name(body: &str) -> Option<String> {
892 let trimmed = body.trim_start();
893 let mut chars = trimmed.char_indices();
894 let (_, first) = chars.next()?;
895 if first == '\'' {
896 let end = trimmed[1..].find('\'')?;
897 return Some(trimmed[1..=end].to_owned());
898 }
899 if !(first.is_ascii_alphanumeric() || first == '_' || first == '@') {
900 return None;
901 }
902 let end = trimmed
903 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '@'))
904 .unwrap_or(trimmed.len());
905 Some(trimmed[..end].to_owned())
906}
907
908pub fn extract_behaviours(src: &str) -> Vec<BehaviourRef> {
912 extract_attribute_atom_argument(src, &["behaviour", "behavior"])
913 .into_iter()
914 .filter_map(|(atom, line)| {
915 ModuleName::new(atom)
916 .ok()
917 .map(|m| BehaviourRef { behaviour: m, line })
918 })
919 .collect()
920}
921
922pub fn extract_includes(src: &str) -> Vec<IncludeRef> {
926 let mut out = Vec::new();
927 for (name, path, line) in extract_attribute_string_argument(src, &["include", "include_lib"]) {
928 let directive = match name.as_str() {
929 "include" => IncludeDirective::Include { path },
930 "include_lib" => IncludeDirective::IncludeLib { path },
931 _ => continue,
932 };
933 out.push(IncludeRef { directive, line });
934 }
935 out
936}
937
938pub fn behaviour_resolves(
941 target: &TargetTreeIndex,
942 search_path_globs: &[String],
943 behaviour: &ModuleName,
944) -> bool {
945 module_resolves(target, search_path_globs, behaviour)
946}
947
948pub fn resolve_include(
956 target: &TargetTreeIndex,
957 source_path: &RelativePath,
958 directive: &IncludeDirective,
959) -> Result<RelativePath, Vec<RelativePath>> {
960 let candidates = include_candidates(source_path, directive);
961 for c in &candidates {
962 if target.contains_path(Path::new(c.as_str())) {
963 return Ok(c.clone());
964 }
965 }
966 Err(candidates)
967}
968
969fn include_candidates(
972 source_path: &RelativePath,
973 directive: &IncludeDirective,
974) -> Vec<RelativePath> {
975 match directive {
976 IncludeDirective::Include { path } => {
977 let source_dir = Path::new(source_path.as_str())
978 .parent()
979 .map(Path::to_path_buf)
980 .unwrap_or_default();
981 let joined = source_dir.join(path);
982 normalised_relative_path(&joined).into_iter().collect()
983 }
984 IncludeDirective::IncludeLib { path } => include_lib_candidates(path),
985 }
986}
987
988fn include_lib_candidates(path: &str) -> Vec<RelativePath> {
994 let mut segs = path.splitn(2, '/');
995 let app = match segs.next() {
996 Some(s) if !s.is_empty() => s,
997 _ => return Vec::new(),
998 };
999 let rest = match segs.next() {
1000 Some(s) => s,
1001 None => return Vec::new(),
1002 };
1003 let mut out = Vec::new();
1004 for prefix in ["deps", "apps", "lib"] {
1005 let candidate = PathBuf::from(prefix).join(app).join(rest);
1006 if let Some(rp) = normalised_relative_path(&candidate) {
1007 out.push(rp);
1008 }
1009 }
1010 out
1011}
1012
1013fn normalised_relative_path(p: &Path) -> Option<RelativePath> {
1014 let s = p.to_string_lossy().replace('\\', "/");
1015 RelativePath::new(s).ok()
1016}
1017
1018fn extract_attribute_atom_argument(src: &str, names: &[&str]) -> Vec<(String, u32)> {
1019 let mut out = Vec::new();
1020 for hit in iter_attribute_bodies(src, names) {
1021 if let Some(atom) = parse_single_atom_argument(hit.body) {
1022 out.push((atom, hit.line));
1023 }
1024 }
1025 out
1026}
1027
1028fn extract_attribute_string_argument(src: &str, names: &[&str]) -> Vec<(String, String, u32)> {
1029 let mut out = Vec::new();
1030 for hit in iter_attribute_bodies(src, names) {
1031 if let Some(path) = parse_single_string_argument(hit.body) {
1032 out.push((hit.name.to_owned(), path, hit.line));
1033 }
1034 }
1035 out
1036}
1037
1038#[derive(Debug, Clone, Copy)]
1039struct AttrHit<'a> {
1040 name: &'a str,
1041 body: &'a str,
1042 line: u32,
1043}
1044
1045fn iter_attribute_bodies<'a>(src: &'a str, names: &'a [&'a str]) -> Vec<AttrHit<'a>> {
1049 let bytes = src.as_bytes();
1050 let mut out = Vec::new();
1051 let mut i = 0;
1052 let mut line = 1u32;
1053 while i < bytes.len() {
1054 let b = bytes[i];
1055 if b == b'\n' {
1056 line += 1;
1057 i += 1;
1058 continue;
1059 }
1060 if b == b'%' {
1061 while i < bytes.len() && bytes[i] != b'\n' {
1062 i += 1;
1063 }
1064 continue;
1065 }
1066 if b == b'"' {
1067 let end = skip_string(bytes, i, b'"');
1068 line += newline_count(&bytes[i..end]);
1069 i = end;
1070 continue;
1071 }
1072 if b == b'\'' {
1073 let end = skip_string(bytes, i, b'\'');
1074 line += newline_count(&bytes[i..end]);
1075 i = end;
1076 continue;
1077 }
1078 if b == b'-'
1079 && let Some(name) = match_attribute_keyword(bytes, i + 1, names)
1080 {
1081 let name_end = i + 1 + name.len();
1082 let mut j = name_end;
1083 while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
1084 j += 1;
1085 }
1086 if j >= bytes.len() || bytes[j] != b'(' {
1087 i = name_end;
1088 continue;
1089 }
1090 let body_start = j + 1;
1091 if let Some(body_end) = match_parens(bytes, body_start) {
1092 let body = str::from_utf8(&bytes[body_start..body_end]).unwrap_or("");
1093 out.push(AttrHit { name, body, line });
1094 line += newline_count(&bytes[i..=body_end]);
1095 i = body_end + 1;
1096 continue;
1097 }
1098 i = name_end;
1099 continue;
1100 }
1101 i += 1;
1102 }
1103 out
1104}
1105
1106fn match_attribute_keyword<'a>(
1107 bytes: &[u8],
1108 start: usize,
1109 names: &'a [&'a str],
1110) -> Option<&'a str> {
1111 for name in names {
1112 let nb = name.as_bytes();
1113 if start + nb.len() > bytes.len() {
1114 continue;
1115 }
1116 if &bytes[start..start + nb.len()] != nb {
1117 continue;
1118 }
1119 let after = start + nb.len();
1120 let next = bytes.get(after).copied().unwrap_or(b' ');
1121 if !is_attribute_name_char(next) {
1122 return Some(name);
1123 }
1124 }
1125 None
1126}
1127
1128fn is_attribute_name_char(b: u8) -> bool {
1129 b.is_ascii_alphanumeric() || b == b'_'
1130}
1131
1132fn match_parens(bytes: &[u8], start: usize) -> Option<usize> {
1133 let mut depth = 1i32;
1134 let mut i = start;
1135 while i < bytes.len() && depth > 0 {
1136 let b = bytes[i];
1137 match b {
1138 b'(' => depth += 1,
1139 b')' => {
1140 depth -= 1;
1141 if depth == 0 {
1142 return Some(i);
1143 }
1144 }
1145 b'"' => {
1146 i = skip_string(bytes, i, b'"');
1147 continue;
1148 }
1149 b'\'' => {
1150 i = skip_string(bytes, i, b'\'');
1151 continue;
1152 }
1153 b'%' => {
1154 while i < bytes.len() && bytes[i] != b'\n' {
1155 i += 1;
1156 }
1157 continue;
1158 }
1159 _ => {}
1160 }
1161 i += 1;
1162 }
1163 None
1164}
1165
1166fn skip_string(bytes: &[u8], start: usize, quote: u8) -> usize {
1167 let mut i = start + 1;
1168 while i < bytes.len() {
1169 if bytes[i] == b'\\' && i + 1 < bytes.len() {
1170 i += 2;
1171 continue;
1172 }
1173 if bytes[i] == quote {
1174 return i + 1;
1175 }
1176 i += 1;
1177 }
1178 i
1179}
1180
1181fn newline_count(span: &[u8]) -> u32 {
1182 let mut n = 0;
1183 for &b in span {
1184 if b == b'\n' {
1185 n += 1;
1186 }
1187 }
1188 n
1189}
1190
1191fn parse_single_atom_argument(body: &str) -> Option<String> {
1192 let trimmed = body.trim();
1193 if trimmed.is_empty() {
1194 return None;
1195 }
1196 let mut chars = trimmed.chars();
1197 let first = chars.next()?;
1198 if first == '\'' {
1199 let end = trimmed[1..].find('\'')?;
1200 return Some(trimmed[1..=end].to_owned());
1201 }
1202 if !first.is_ascii_lowercase() {
1203 return None;
1204 }
1205 let mut end = 1;
1206 for c in chars {
1207 if c.is_ascii_alphanumeric() || c == '_' || c == '@' {
1208 end += c.len_utf8();
1209 continue;
1210 }
1211 break;
1212 }
1213 Some(trimmed[..end].to_owned())
1214}
1215
1216fn parse_single_string_argument(body: &str) -> Option<String> {
1217 let trimmed = body.trim();
1218 let first = trimmed.chars().next()?;
1219 if first != '"' {
1220 return None;
1221 }
1222 let bytes = trimmed.as_bytes();
1223 let mut i = 1usize;
1224 let mut out = String::new();
1225 while i < bytes.len() {
1226 let b = bytes[i];
1227 if b == b'\\' && i + 1 < bytes.len() {
1228 out.push(bytes[i + 1] as char);
1229 i += 2;
1230 continue;
1231 }
1232 if b == b'"' {
1233 return Some(out);
1234 }
1235 out.push(b as char);
1236 i += 1;
1237 }
1238 None
1239}