1use crate::ast::{Node, NodeKind};
22use perl_semantic_facts::{
23 AnchorId, Confidence, FileId, ImportKind, ImportSpec, ImportSymbols, Provenance,
24};
25
26pub struct ImportExtractor;
29
30impl ImportExtractor {
31 pub fn extract(ast: &Node, file_id: FileId) -> Vec<ImportSpec> {
37 let mut specs = Vec::new();
38 Self::walk(ast, file_id, &mut specs);
39 specs
40 }
41
42 fn walk(node: &Node, file_id: FileId, out: &mut Vec<ImportSpec>) {
45 if let NodeKind::Use { module, args, .. } = &node.kind {
47 if let Some(spec) = Self::classify_use(module, args, file_id, node) {
48 out.push(spec);
49 }
50 }
51
52 match &node.kind {
56 NodeKind::Program { statements } | NodeKind::Block { statements } => {
57 Self::walk_statements(statements, file_id, out);
58 }
59 NodeKind::Package { block: Some(block), .. } => {
60 if let NodeKind::Block { statements } = &block.kind {
61 Self::walk_statements(statements, file_id, out);
62 }
63 }
64 _ => {}
65 }
66
67 for child in node.children() {
68 Self::walk(child, file_id, out);
69 }
70 }
71
72 fn walk_statements(statements: &[Node], file_id: FileId, out: &mut Vec<ImportSpec>) {
82 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
85
86 for (i, stmt) in statements.iter().enumerate() {
87 if consumed.contains(&i) {
88 continue;
89 }
90
91 let expr = Self::unwrap_expression_statement(stmt);
93
94 let (require_node, require_args) = match &expr.kind {
96 NodeKind::FunctionCall { name, args } if name == "require" => (stmt, args),
97 _ => continue,
98 };
99
100 if Self::is_dynamic_require(require_args) {
102 out.push(Self::make_dynamic_require(file_id, require_node));
103 consumed.insert(i);
104 continue;
105 }
106
107 let module_name = match Self::extract_require_module_name(require_args) {
109 Some(name) => name,
110 None => continue,
111 };
112
113 let import_spec = if let Some(next_stmt) = statements.get(i + 1) {
115 let next_expr = Self::unwrap_expression_statement(next_stmt);
116 Self::try_match_import_call(next_expr, &module_name)
117 } else {
118 None
119 };
120
121 if let Some((symbols, import_node)) = import_spec {
122 let anchor_id = Self::anchor_from_node(require_node);
126 let confidence = Self::confidence_for_symbols(&symbols);
127 out.push(ImportSpec {
128 module: module_name,
129 kind: ImportKind::RequireThenImport,
130 symbols,
131 provenance: Provenance::ExactAst,
132 confidence,
133 file_id: Some(file_id),
134 anchor_id: Some(anchor_id),
135 scope_id: None,
136 });
137 consumed.insert(i);
138 consumed.insert(i + 1);
139 let _ = import_node;
142 } else {
143 let anchor_id = Self::anchor_from_node(require_node);
145 out.push(ImportSpec {
146 module: module_name,
147 kind: ImportKind::Require,
148 symbols: ImportSymbols::Default,
149 provenance: Provenance::ExactAst,
150 confidence: Confidence::High,
151 file_id: Some(file_id),
152 anchor_id: Some(anchor_id),
153 scope_id: None,
154 });
155 consumed.insert(i);
156 }
157 }
158 }
159
160 fn unwrap_expression_statement(node: &Node) -> &Node {
165 match &node.kind {
166 NodeKind::ExpressionStatement { expression } => expression,
167 _ => node,
168 }
169 }
170
171 fn is_dynamic_require(args: &[Node]) -> bool {
174 match args.first() {
175 Some(arg) => matches!(&arg.kind, NodeKind::Variable { .. }),
176 None => false,
177 }
178 }
179
180 fn extract_require_module_name(args: &[Node]) -> Option<String> {
186 let arg = args.first()?;
187 match &arg.kind {
188 NodeKind::Identifier { name } => Some(name.clone()),
189 NodeKind::String { value, .. } => {
190 let cleaned = value.trim_matches('\'').trim_matches('"').trim();
192 let module = cleaned.trim_end_matches(".pm").replace('/', "::");
193 Some(module)
194 }
195 _ => None,
196 }
197 }
198
199 fn make_dynamic_require(file_id: FileId, node: &Node) -> ImportSpec {
207 let anchor_id = Self::anchor_from_node(node);
208 ImportSpec {
209 module: String::new(),
210 kind: ImportKind::DynamicRequire,
211 symbols: ImportSymbols::Dynamic,
212 provenance: Provenance::DynamicBoundary,
213 confidence: Confidence::Low,
214 file_id: Some(file_id),
215 anchor_id: Some(anchor_id),
216 scope_id: None,
217 }
218 }
219
220 fn try_match_import_call<'a>(
225 node: &'a Node,
226 expected_module: &str,
227 ) -> Option<(ImportSymbols, &'a Node)> {
228 let (object, method, args) = match &node.kind {
229 NodeKind::MethodCall { object, method, args } => (object, method, args),
230 _ => return None,
231 };
232
233 if method != "import" {
234 return None;
235 }
236
237 let obj_name = match &object.kind {
239 NodeKind::Identifier { name } => name.as_str(),
240 _ => return None,
241 };
242
243 if obj_name != expected_module {
244 return None;
245 }
246
247 let symbols = Self::extract_import_call_symbols(args);
249 Some((symbols, node))
250 }
251
252 fn extract_import_call_symbols(args: &[Node]) -> ImportSymbols {
254 if args.is_empty() {
255 return ImportSymbols::Default;
256 }
257
258 let mut names: Vec<String> = Vec::new();
259 let mut tags: Vec<String> = Vec::new();
260 let mut has_dynamic_arg = false;
261
262 for arg in args {
263 has_dynamic_arg |= Self::collect_import_arg_symbols(arg, &mut names, &mut tags);
264 }
265
266 if has_dynamic_arg {
267 return ImportSymbols::Dynamic;
268 }
269
270 if names.is_empty() && tags.is_empty() {
271 return ImportSymbols::Default;
272 }
273
274 if !tags.is_empty() && names.is_empty() {
275 return ImportSymbols::Tags(tags);
276 }
277
278 if !tags.is_empty() && !names.is_empty() {
279 return ImportSymbols::Mixed { tags, names };
280 }
281
282 ImportSymbols::Explicit(names)
283 }
284
285 fn collect_import_arg_symbols(
291 arg: &Node,
292 names: &mut Vec<String>,
293 tags: &mut Vec<String>,
294 ) -> bool {
295 match &arg.kind {
296 NodeKind::String { value, .. } => {
297 let bare = value.trim_matches('\'').trim_matches('"');
298 if let Some(tag) = bare.strip_prefix(':') {
299 tags.push(tag.to_string());
300 } else if !bare.is_empty() {
301 names.push(bare.to_string());
302 }
303 false
304 }
305 NodeKind::Identifier { name } => {
306 if let Some(inner) = Self::parse_qw_content(name) {
308 for word in inner.split_whitespace() {
309 if let Some(tag) = word.strip_prefix(':') {
310 tags.push(tag.to_string());
311 } else {
312 names.push(word.to_string());
313 }
314 }
315 } else if let Some(tag) = name.strip_prefix(':') {
316 tags.push(tag.to_string());
317 } else if !name.is_empty() {
318 names.push(name.clone());
319 }
320 false
321 }
322 NodeKind::Variable { .. } => {
323 true
326 }
327 NodeKind::ArrayLiteral { elements } => {
328 let mut has_dynamic_arg = false;
330 for el in elements {
331 has_dynamic_arg |= Self::collect_import_arg_symbols(el, names, tags);
332 }
333 has_dynamic_arg
334 }
335 _ => true,
336 }
337 }
338
339 fn confidence_for_symbols(symbols: &ImportSymbols) -> Confidence {
340 if matches!(symbols, ImportSymbols::Dynamic) { Confidence::Low } else { Confidence::High }
341 }
342
343 fn classify_use(
350 module: &str,
351 args: &[String],
352 file_id: FileId,
353 node: &Node,
354 ) -> Option<ImportSpec> {
355 if Self::is_version_pragma(module) {
357 return None;
358 }
359
360 let anchor_id = Self::anchor_from_node(node);
361
362 if module == "constant" {
364 return Some(Self::classify_use_constant(args, file_id, anchor_id));
365 }
366
367 let (kind, symbols) = Self::classify_args(args, module, node);
369
370 Some(ImportSpec {
371 module: module.to_string(),
372 kind,
373 symbols,
374 provenance: Provenance::ExactAst,
375 confidence: Confidence::High,
376 file_id: Some(file_id),
377 anchor_id: Some(anchor_id),
378 scope_id: None,
379 })
380 }
381
382 fn classify_args(args: &[String], module: &str, node: &Node) -> (ImportKind, ImportSymbols) {
384 if args.is_empty() {
385 let bare_len = "use ".len() + module.len() + 1; let span_len = node.location.end.saturating_sub(node.location.start);
392 if span_len > bare_len {
393 return (ImportKind::UseEmpty, ImportSymbols::None);
396 }
397 return (ImportKind::Use, ImportSymbols::Default);
399 }
400
401 let mut explicit_names: Vec<String> = Vec::new();
403 let mut tags: Vec<String> = Vec::new();
404
405 for arg in args {
406 let trimmed = arg.trim();
407
408 if let Some(inner) = Self::parse_qw_content(trimmed) {
410 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
411 for word in words {
412 if let Some(tag) = word.strip_prefix(':') {
413 tags.push(tag.to_string());
414 } else {
415 explicit_names.push(word);
416 }
417 }
418 continue;
419 }
420
421 let unquoted = Self::unquote(trimmed);
423 if let Some(tag) = unquoted.strip_prefix(':') {
424 tags.push(tag.to_string());
425 continue;
426 }
427
428 if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
431 continue;
432 }
433
434 if Self::looks_like_symbol_name(trimmed) {
436 explicit_names.push(Self::unquote(trimmed).to_string());
437 }
438 }
439
440 if explicit_names.is_empty() && tags.is_empty() && !args.is_empty() {
442 let has_any_symbol = args.iter().any(|a| {
446 let t = a.trim();
447 Self::looks_like_symbol_name(t) || Self::parse_qw_content(t).is_some()
448 });
449 if !has_any_symbol {
450 return (ImportKind::UseEmpty, ImportSymbols::None);
451 }
452 }
453
454 if !tags.is_empty() && explicit_names.is_empty() {
456 return (ImportKind::UseTag, ImportSymbols::Tags(tags));
457 }
458
459 if !tags.is_empty() && !explicit_names.is_empty() {
461 return (
462 ImportKind::UseExplicitList,
463 ImportSymbols::Mixed { tags, names: explicit_names },
464 );
465 }
466
467 if !explicit_names.is_empty() {
469 return (ImportKind::UseExplicitList, ImportSymbols::Explicit(explicit_names));
470 }
471
472 (ImportKind::Use, ImportSymbols::Default)
474 }
475
476 fn classify_use_constant(args: &[String], file_id: FileId, anchor_id: AnchorId) -> ImportSpec {
478 let mut constant_names: Vec<String> = Vec::new();
479
480 if args.is_empty() {
481 return ImportSpec {
483 module: "constant".to_string(),
484 kind: ImportKind::UseConstant,
485 symbols: ImportSymbols::None,
486 provenance: Provenance::ExactAst,
487 confidence: Confidence::High,
488 file_id: Some(file_id),
489 anchor_id: Some(anchor_id),
490 scope_id: None,
491 };
492 }
493
494 if args.first().map(|a| a.as_str()) == Some("{") {
497 let mut i = 1; while i < args.len() {
499 let token = args[i].trim();
500 if token == "}" || token == "=>" || token == "," {
501 i += 1;
502 continue;
503 }
504 if i + 1 < args.len() && args[i + 1].trim() == "=>" {
506 constant_names.push(token.to_string());
507 i += 3;
509 } else {
510 i += 1;
511 }
512 }
513 }
514 else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
516 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
517 constant_names.extend(words);
518 }
519 else if let Some(name) = args.first() {
522 let trimmed = name.trim();
523 if Self::looks_like_constant_name(trimmed) {
524 constant_names.push(trimmed.to_string());
525 }
526 }
527
528 let mut seen = std::collections::HashSet::new();
530 constant_names.retain(|n| seen.insert(n.clone()));
531
532 let symbols = if constant_names.is_empty() {
533 ImportSymbols::None
534 } else {
535 ImportSymbols::Explicit(constant_names)
536 };
537
538 ImportSpec {
539 module: "constant".to_string(),
540 kind: ImportKind::UseConstant,
541 symbols,
542 provenance: Provenance::ExactAst,
543 confidence: Confidence::High,
544 file_id: Some(file_id),
545 anchor_id: Some(anchor_id),
546 scope_id: None,
547 }
548 }
549
550 fn anchor_from_node(node: &Node) -> AnchorId {
554 AnchorId(node.location.start as u64)
557 }
558
559 fn is_version_pragma(module: &str) -> bool {
561 if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
563 return true;
564 }
565 if module.starts_with('v')
567 && module.len() > 1
568 && module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
569 {
570 return true;
571 }
572 false
573 }
574
575 fn parse_qw_content(s: &str) -> Option<&str> {
579 let rest = s.strip_prefix("qw")?;
580 let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
582 Some(inner)
583 }
584
585 fn unquote(s: &str) -> &str {
587 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
588 if s.len() >= 2 {
589 return &s[1..s.len() - 1];
590 }
591 }
592 s
593 }
594
595 fn looks_like_symbol_name(s: &str) -> bool {
597 let s = Self::unquote(s);
598 if s.is_empty() {
599 return false;
600 }
601 if s.starts_with(':') {
603 return true;
604 }
605 if s.starts_with('$')
607 || s.starts_with('@')
608 || s.starts_with('%')
609 || s.starts_with('&')
610 || s.starts_with('*')
611 {
612 return true;
613 }
614 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
616 }
617
618 fn looks_like_constant_name(s: &str) -> bool {
622 if s.is_empty() {
623 return false;
624 }
625 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use crate::Parser;
633
634 fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
636 let mut parser = Parser::new(code);
637 let ast = match parser.parse() {
638 Ok(ast) => ast,
639 Err(_) => return Vec::new(),
640 };
641 ImportExtractor::extract(&ast, FileId(1))
642 }
643
644 #[test]
647 fn test_use_explicit_list_qw() -> Result<(), String> {
648 let specs = parse_and_extract("use List::Util qw(first reduce any);");
649 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
650
651 assert_eq!(spec.module, "List::Util");
652 assert_eq!(spec.kind, ImportKind::UseExplicitList);
653 if let ImportSymbols::Explicit(names) = &spec.symbols {
654 assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
655 assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
656 assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
657 } else {
658 return Err(format!("expected Explicit, got {:?}", spec.symbols));
659 }
660 assert_eq!(spec.file_id, Some(FileId(1)));
661 assert!(spec.anchor_id.is_some());
662 Ok(())
663 }
664
665 #[test]
666 fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
667 let specs = parse_and_extract("use Exporter 'import';");
668 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
669
670 assert_eq!(spec.module, "Exporter");
671 assert_eq!(spec.kind, ImportKind::UseExplicitList);
672 if let ImportSymbols::Explicit(names) = &spec.symbols {
673 assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
674 } else {
675 return Err(format!("expected Explicit, got {:?}", spec.symbols));
676 }
677 Ok(())
678 }
679
680 #[test]
688 fn test_use_empty_parens() -> Result<(), String> {
689 let specs = parse_and_extract("use POSIX ();");
690 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
691
692 assert_eq!(spec.module, "POSIX");
693 assert_eq!(spec.kind, ImportKind::UseEmpty);
697 assert_eq!(spec.symbols, ImportSymbols::None);
698 Ok(())
699 }
700
701 #[test]
704 fn test_use_tag_single() -> Result<(), String> {
705 let specs = parse_and_extract("use POSIX ':sys_wait_h';");
706 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
707
708 assert_eq!(spec.module, "POSIX");
709 assert_eq!(spec.kind, ImportKind::UseTag);
710 if let ImportSymbols::Tags(tags) = &spec.symbols {
711 assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
712 } else {
713 return Err(format!("expected Tags, got {:?}", spec.symbols));
714 }
715 Ok(())
716 }
717
718 #[test]
719 fn test_use_tag_in_qw() -> Result<(), String> {
720 let specs = parse_and_extract("use Fcntl qw(:flock);");
721 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
722
723 assert_eq!(spec.module, "Fcntl");
724 assert_eq!(spec.kind, ImportKind::UseTag);
725 if let ImportSymbols::Tags(tags) = &spec.symbols {
726 assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
727 } else {
728 return Err(format!("expected Tags, got {:?}", spec.symbols));
729 }
730 Ok(())
731 }
732
733 #[test]
736 fn test_use_bare() -> Result<(), String> {
737 let specs = parse_and_extract("use strict;");
738 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
739
740 assert_eq!(spec.module, "strict");
741 assert_eq!(spec.kind, ImportKind::Use);
742 assert_eq!(spec.symbols, ImportSymbols::Default);
743 Ok(())
744 }
745
746 #[test]
747 fn test_use_bare_qualified() -> Result<(), String> {
748 let specs = parse_and_extract("use Data::Dumper;");
749 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
750
751 assert_eq!(spec.module, "Data::Dumper");
752 assert_eq!(spec.kind, ImportKind::Use);
753 assert_eq!(spec.symbols, ImportSymbols::Default);
754 Ok(())
755 }
756
757 #[test]
760 fn test_use_constant_scalar() -> Result<(), String> {
761 let specs = parse_and_extract("use constant PI => 3.14;");
762 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
763
764 assert_eq!(spec.module, "constant");
765 assert_eq!(spec.kind, ImportKind::UseConstant);
766 if let ImportSymbols::Explicit(names) = &spec.symbols {
767 assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
768 } else {
769 return Err(format!("expected Explicit, got {:?}", spec.symbols));
770 }
771 Ok(())
772 }
773
774 #[test]
775 fn test_use_constant_hash_ref() -> Result<(), String> {
776 let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
777 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
778
779 assert_eq!(spec.module, "constant");
780 assert_eq!(spec.kind, ImportKind::UseConstant);
781 if let ImportSymbols::Explicit(names) = &spec.symbols {
782 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
783 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
784 } else {
785 return Err(format!("expected Explicit, got {:?}", spec.symbols));
786 }
787 Ok(())
788 }
789
790 #[test]
791 fn test_use_constant_empty() -> Result<(), String> {
792 let specs = parse_and_extract("use constant;");
793 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
794
795 assert_eq!(spec.module, "constant");
796 assert_eq!(spec.kind, ImportKind::UseConstant);
797 assert_eq!(spec.symbols, ImportSymbols::None);
798 Ok(())
799 }
800
801 #[test]
804 fn test_version_pragma_skipped() -> Result<(), String> {
805 let specs = parse_and_extract("use 5.036;");
806 assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
807 Ok(())
808 }
809
810 #[test]
811 fn test_vstring_pragma_skipped() -> Result<(), String> {
812 let specs = parse_and_extract("use v5.38;");
813 assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
814 Ok(())
815 }
816
817 #[test]
820 fn test_multiple_use_statements() -> Result<(), String> {
821 let code = r#"
822use strict;
823use warnings;
824use List::Util qw(first any);
825use POSIX ();
826use constant MAX => 100;
827"#;
828 let specs = parse_and_extract(code);
829 assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
830
831 assert_eq!(specs[0].module, "strict");
833 assert_eq!(specs[0].kind, ImportKind::Use);
834
835 assert_eq!(specs[1].module, "warnings");
837 assert_eq!(specs[1].kind, ImportKind::Use);
838
839 assert_eq!(specs[2].module, "List::Util");
841 assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
842
843 assert_eq!(specs[3].module, "POSIX");
845 assert_eq!(specs[3].kind, ImportKind::UseEmpty);
846
847 assert_eq!(specs[4].module, "constant");
849 assert_eq!(specs[4].kind, ImportKind::UseConstant);
850
851 Ok(())
852 }
853
854 #[test]
857 fn test_anchor_and_file_id_populated() -> Result<(), String> {
858 let specs = parse_and_extract("use Foo::Bar qw(baz);");
859 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
860
861 assert_eq!(spec.file_id, Some(FileId(1)));
862 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
863 assert_eq!(spec.provenance, Provenance::ExactAst);
864 assert_eq!(spec.confidence, Confidence::High);
865 Ok(())
866 }
867
868 #[test]
871 fn test_use_inside_package_block() -> Result<(), String> {
872 let code = r#"
873package MyModule;
874use Exporter 'import';
875our @EXPORT = qw(foo);
8761;
877"#;
878 let specs = parse_and_extract(code);
879 let exporter_spec =
880 specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
881
882 assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
883 if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
884 assert!(names.contains(&"import".to_string()));
885 } else {
886 return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
887 }
888 Ok(())
889 }
890
891 #[test]
894 fn test_use_mixed_tags_and_names() -> Result<(), String> {
895 let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
896 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
897
898 assert_eq!(spec.module, "Fcntl");
899 assert_eq!(spec.kind, ImportKind::UseExplicitList);
900 if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
901 assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
902 assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
903 assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
904 } else {
905 return Err(format!("expected Mixed, got {:?}", spec.symbols));
906 }
907 Ok(())
908 }
909
910 #[test]
913 fn test_require_bare_module() -> Result<(), String> {
914 let specs = parse_and_extract("require Foo::Bar;");
915 let spec = specs
916 .iter()
917 .find(|s| s.module == "Foo::Bar")
918 .ok_or("expected ImportSpec for Foo::Bar")?;
919
920 assert_eq!(spec.kind, ImportKind::Require);
921 assert_eq!(spec.symbols, ImportSymbols::Default);
922 assert_eq!(spec.provenance, Provenance::ExactAst);
923 assert_eq!(spec.confidence, Confidence::High);
924 assert_eq!(spec.file_id, Some(FileId(1)));
925 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
926 Ok(())
927 }
928
929 #[test]
932 fn test_require_then_import_with_qw() -> Result<(), String> {
933 let code = r#"
934require Foo::Bar;
935Foo::Bar->import(qw(alpha beta));
936"#;
937 let specs = parse_and_extract(code);
938 let spec = specs
939 .iter()
940 .find(|s| s.module == "Foo::Bar")
941 .ok_or("expected ImportSpec for Foo::Bar")?;
942
943 assert_eq!(spec.kind, ImportKind::RequireThenImport);
944 if let ImportSymbols::Explicit(names) = &spec.symbols {
945 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
946 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
947 } else {
948 return Err(format!("expected Explicit, got {:?}", spec.symbols));
949 }
950 assert_eq!(spec.provenance, Provenance::ExactAst);
951 assert_eq!(spec.confidence, Confidence::High);
952 Ok(())
953 }
954
955 #[test]
956 fn test_require_then_import_bare() -> Result<(), String> {
957 let code = r#"
958require Some::Module;
959Some::Module->import();
960"#;
961 let specs = parse_and_extract(code);
962 let spec = specs
963 .iter()
964 .find(|s| s.module == "Some::Module")
965 .ok_or("expected ImportSpec for Some::Module")?;
966
967 assert_eq!(spec.kind, ImportKind::RequireThenImport);
968 assert_eq!(spec.symbols, ImportSymbols::Default);
969 Ok(())
970 }
971
972 #[test]
973 fn test_require_then_import_quoted_strings() -> Result<(), String> {
974 let code = r#"
975require Foo::Bar;
976Foo::Bar->import('alpha', 'beta');
977"#;
978 let specs = parse_and_extract(code);
979 let spec = specs
980 .iter()
981 .find(|s| s.module == "Foo::Bar")
982 .ok_or("expected ImportSpec for Foo::Bar")?;
983
984 assert_eq!(spec.kind, ImportKind::RequireThenImport);
985 if let ImportSymbols::Explicit(names) = &spec.symbols {
986 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
987 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
988 } else {
989 return Err(format!("expected Explicit, got {:?}", spec.symbols));
990 }
991 assert_eq!(spec.confidence, Confidence::High);
992 Ok(())
993 }
994
995 #[test]
996 fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
997 let code = r#"
998require Foo::Bar;
999Foo::Bar->import(@names);
1000"#;
1001 let specs = parse_and_extract(code);
1002 let spec = specs
1003 .iter()
1004 .find(|s| s.module == "Foo::Bar")
1005 .ok_or("expected ImportSpec for Foo::Bar")?;
1006
1007 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1008 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1009 assert_eq!(spec.confidence, Confidence::Low);
1010 Ok(())
1011 }
1012
1013 #[test]
1016 fn test_require_dynamic_variable() -> Result<(), String> {
1017 let specs = parse_and_extract("require $module;");
1018 let spec = specs
1019 .iter()
1020 .find(|s| s.kind == ImportKind::DynamicRequire)
1021 .ok_or("expected DynamicRequire ImportSpec")?;
1022
1023 assert_eq!(spec.module, "");
1024 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1025 assert_eq!(spec.provenance, Provenance::DynamicBoundary);
1028 assert_eq!(spec.confidence, Confidence::Low);
1029 assert_eq!(spec.file_id, Some(FileId(1)));
1030 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1031 Ok(())
1032 }
1033
1034 #[test]
1037 fn test_mixed_use_and_require() -> Result<(), String> {
1038 let code = r#"
1039use strict;
1040use warnings;
1041require Foo::Bar;
1042Foo::Bar->import(qw(baz));
1043require $dynamic;
1044"#;
1045 let specs = parse_and_extract(code);
1046
1047 let strict_spec =
1049 specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
1050 assert_eq!(strict_spec.kind, ImportKind::Use);
1051
1052 let warnings_spec =
1054 specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
1055 assert_eq!(warnings_spec.kind, ImportKind::Use);
1056
1057 let foo_spec =
1059 specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
1060 assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
1061 if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
1062 assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
1063 } else {
1064 return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
1065 }
1066
1067 let dyn_spec = specs
1069 .iter()
1070 .find(|s| s.kind == ImportKind::DynamicRequire)
1071 .ok_or("expected DynamicRequire ImportSpec")?;
1072 assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
1073
1074 Ok(())
1075 }
1076
1077 #[test]
1080 fn test_require_string_path() -> Result<(), String> {
1081 let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
1082 let spec = specs
1083 .iter()
1084 .find(|s| s.module == "Foo::Bar")
1085 .ok_or("expected ImportSpec for Foo::Bar")?;
1086
1087 assert_eq!(spec.kind, ImportKind::Require);
1088 assert_eq!(spec.symbols, ImportSymbols::Default);
1089 Ok(())
1090 }
1091}