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 if let Some(spec) = Self::try_classify_standalone_class_import(node, file_id) {
63 out.push(spec);
64 }
65
66 match &node.kind {
70 NodeKind::Program { statements } | NodeKind::Block { statements } => {
71 Self::walk_statements(statements, file_id, out);
72 }
73 NodeKind::Package { block: Some(block), .. } => {
74 if let NodeKind::Block { statements } = &block.kind {
75 Self::walk_statements(statements, file_id, out);
76 }
77 }
78 _ => {}
79 }
80
81 for child in node.children() {
82 Self::walk(child, file_id, out);
83 }
84 }
85
86 fn try_classify_standalone_class_import(node: &Node, file_id: FileId) -> Option<ImportSpec> {
101 let (object, method, args) = match &node.kind {
102 NodeKind::MethodCall { object, method, args } => (object, method, args),
103 _ => return None,
104 };
105
106 if method != "import" {
107 return None;
108 }
109
110 let class_name = match &object.kind {
112 NodeKind::Identifier { name } => name.as_str(),
113 _ => return None,
114 };
115
116 let symbols = Self::extract_import_call_symbols(args);
118
119 if !matches!(symbols, ImportSymbols::Dynamic) {
123 return None;
124 }
125
126 let anchor_id = Self::anchor_from_node(node);
127 Some(ImportSpec {
128 module: class_name.to_string(),
129 kind: ImportKind::ManualImport,
132 symbols,
133 provenance: Provenance::DynamicBoundary,
134 confidence: Confidence::Low,
135 file_id: Some(file_id),
136 anchor_id: Some(anchor_id),
137 scope_id: None,
138 span_start_byte: Some(node.location.start as u32),
139 })
140 }
141
142 fn walk_statements(statements: &[Node], file_id: FileId, out: &mut Vec<ImportSpec>) {
152 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
155
156 for (i, stmt) in statements.iter().enumerate() {
157 if consumed.contains(&i) {
158 continue;
159 }
160
161 let expr = Self::unwrap_expression_statement(stmt);
163
164 let (require_node, require_args) = match &expr.kind {
166 NodeKind::FunctionCall { name, args } if name == "require" => (stmt, args),
167 _ => continue,
168 };
169
170 if Self::is_dynamic_require(require_args) {
172 out.push(Self::make_dynamic_require(file_id, require_node));
173 consumed.insert(i);
174 continue;
175 }
176
177 let module_name = match Self::extract_require_module_name(require_args) {
179 Some(name) => name,
180 None => continue,
181 };
182
183 let import_spec = if let Some(next_stmt) = statements.get(i + 1) {
185 let next_expr = Self::unwrap_expression_statement(next_stmt);
186 Self::try_match_import_call(next_expr, &module_name)
187 } else {
188 None
189 };
190
191 if let Some((symbols, import_node)) = import_spec {
192 let provenance = if matches!(symbols, ImportSymbols::Dynamic) {
201 Provenance::ExactAst
202 } else {
203 Provenance::LiteralRequireImport
204 };
205 let anchor_id = Self::anchor_from_node(require_node);
206 let confidence = Self::confidence_for_symbols(&symbols);
207 out.push(ImportSpec {
208 module: module_name,
209 kind: ImportKind::RequireThenImport,
210 symbols,
211 provenance,
212 confidence,
213 file_id: Some(file_id),
214 anchor_id: Some(anchor_id),
215 scope_id: None,
216 span_start_byte: Some(require_node.location.start as u32),
217 });
218 consumed.insert(i);
219 consumed.insert(i + 1);
220 let _ = import_node;
223 } else {
224 let anchor_id = Self::anchor_from_node(require_node);
226 out.push(ImportSpec {
227 module: module_name,
228 kind: ImportKind::Require,
229 symbols: ImportSymbols::Default,
230 provenance: Provenance::ExactAst,
231 confidence: Confidence::High,
232 file_id: Some(file_id),
233 anchor_id: Some(anchor_id),
234 scope_id: None,
235 span_start_byte: Some(require_node.location.start as u32),
236 });
237 consumed.insert(i);
238 }
239 }
240 }
241
242 fn unwrap_expression_statement(node: &Node) -> &Node {
247 match &node.kind {
248 NodeKind::ExpressionStatement { expression } => expression,
249 _ => node,
250 }
251 }
252
253 fn is_dynamic_require(args: &[Node]) -> bool {
256 match args.first() {
257 Some(arg) => matches!(&arg.kind, NodeKind::Variable { .. }),
258 None => false,
259 }
260 }
261
262 fn extract_require_module_name(args: &[Node]) -> Option<String> {
268 let arg = args.first()?;
269 match &arg.kind {
270 NodeKind::Identifier { name } => Some(name.clone()),
271 NodeKind::String { value, .. } => {
272 let cleaned = value.trim_matches('\'').trim_matches('"').trim();
274 let module = cleaned.trim_end_matches(".pm").replace('/', "::");
275 Some(module)
276 }
277 _ => None,
278 }
279 }
280
281 fn make_dynamic_require(file_id: FileId, node: &Node) -> ImportSpec {
289 let anchor_id = Self::anchor_from_node(node);
290 ImportSpec {
291 module: String::new(),
292 kind: ImportKind::DynamicRequire,
293 symbols: ImportSymbols::Dynamic,
294 provenance: Provenance::DynamicBoundary,
295 confidence: Confidence::Low,
296 file_id: Some(file_id),
297 anchor_id: Some(anchor_id),
298 scope_id: None,
299 span_start_byte: Some(node.location.start as u32),
300 }
301 }
302
303 fn try_match_import_call<'a>(
308 node: &'a Node,
309 expected_module: &str,
310 ) -> Option<(ImportSymbols, &'a Node)> {
311 let (object, method, args) = match &node.kind {
312 NodeKind::MethodCall { object, method, args } => (object, method, args),
313 _ => return None,
314 };
315
316 if method != "import" {
317 return None;
318 }
319
320 let obj_name = match &object.kind {
322 NodeKind::Identifier { name } => name.as_str(),
323 _ => return None,
324 };
325
326 if obj_name != expected_module {
327 return None;
328 }
329
330 let symbols = Self::extract_import_call_symbols(args);
332 Some((symbols, node))
333 }
334
335 fn extract_import_call_symbols(args: &[Node]) -> ImportSymbols {
337 if args.is_empty() {
338 return ImportSymbols::Default;
339 }
340
341 let mut names: Vec<String> = Vec::new();
342 let mut tags: Vec<String> = Vec::new();
343 let mut has_dynamic_arg = false;
344
345 for arg in args {
346 has_dynamic_arg |= Self::collect_import_arg_symbols(arg, &mut names, &mut tags);
347 }
348
349 if has_dynamic_arg {
350 return ImportSymbols::Dynamic;
351 }
352
353 if names.is_empty() && tags.is_empty() {
354 return ImportSymbols::Default;
355 }
356
357 if !tags.is_empty() && names.is_empty() {
358 return ImportSymbols::Tags(tags);
359 }
360
361 if !tags.is_empty() && !names.is_empty() {
362 return ImportSymbols::Mixed { tags, names };
363 }
364
365 ImportSymbols::Explicit(names)
366 }
367
368 fn collect_import_arg_symbols(
374 arg: &Node,
375 names: &mut Vec<String>,
376 tags: &mut Vec<String>,
377 ) -> bool {
378 match &arg.kind {
379 NodeKind::String { value, .. } => {
380 let bare = value.trim_matches('\'').trim_matches('"');
381 if let Some(tag) = bare.strip_prefix(':') {
382 tags.push(tag.to_string());
383 } else if !bare.is_empty() {
384 names.push(bare.to_string());
385 }
386 false
387 }
388 NodeKind::Identifier { name } => {
389 if let Some(inner) = Self::parse_qw_content(name) {
391 for word in inner.split_whitespace() {
392 if let Some(tag) = word.strip_prefix(':') {
393 tags.push(tag.to_string());
394 } else {
395 names.push(word.to_string());
396 }
397 }
398 } else if let Some(tag) = name.strip_prefix(':') {
399 tags.push(tag.to_string());
400 } else if !name.is_empty() {
401 names.push(name.clone());
402 }
403 false
404 }
405 NodeKind::Variable { .. } => {
406 true
409 }
410 NodeKind::ArrayLiteral { elements } => {
411 let mut has_dynamic_arg = false;
413 for el in elements {
414 has_dynamic_arg |= Self::collect_import_arg_symbols(el, names, tags);
415 }
416 has_dynamic_arg
417 }
418 _ => true,
419 }
420 }
421
422 fn confidence_for_symbols(symbols: &ImportSymbols) -> Confidence {
423 if matches!(symbols, ImportSymbols::Dynamic) { Confidence::Low } else { Confidence::High }
424 }
425
426 fn classify_use(
433 module: &str,
434 args: &[String],
435 file_id: FileId,
436 node: &Node,
437 ) -> Option<ImportSpec> {
438 if Self::is_version_pragma(module) {
440 return None;
441 }
442
443 let anchor_id = Self::anchor_from_node(node);
444
445 if module == "constant" {
447 return Some(Self::classify_use_constant(args, file_id, anchor_id));
448 }
449
450 let (kind, symbols) = Self::classify_args(args, module, node);
452
453 Some(ImportSpec {
454 module: module.to_string(),
455 kind,
456 symbols,
457 provenance: Provenance::ExactAst,
458 confidence: Confidence::High,
459 file_id: Some(file_id),
460 anchor_id: Some(anchor_id),
461 scope_id: None,
462 span_start_byte: Some(node.location.start as u32),
463 })
464 }
465
466 fn classify_args(args: &[String], module: &str, node: &Node) -> (ImportKind, ImportSymbols) {
468 if args.is_empty() {
469 let bare_len = "use ".len() + module.len() + 1; let span_len = node.location.end.saturating_sub(node.location.start);
476 if span_len > bare_len {
477 return (ImportKind::UseEmpty, ImportSymbols::None);
480 }
481 return (ImportKind::Use, ImportSymbols::Default);
483 }
484
485 let mut explicit_names: Vec<String> = Vec::new();
487 let mut tags: Vec<String> = Vec::new();
488
489 for arg in args {
490 let trimmed = arg.trim();
491
492 if let Some(inner) = Self::parse_qw_content(trimmed) {
494 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
495 for word in words {
496 if let Some(tag) = word.strip_prefix(':') {
497 tags.push(tag.to_string());
498 } else {
499 explicit_names.push(word);
500 }
501 }
502 continue;
503 }
504
505 let unquoted = Self::unquote(trimmed);
507 if let Some(tag) = unquoted.strip_prefix(':') {
508 tags.push(tag.to_string());
509 continue;
510 }
511
512 if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
515 continue;
516 }
517
518 if Self::looks_like_symbol_name(trimmed) {
520 explicit_names.push(Self::unquote(trimmed).to_string());
521 }
522 }
523
524 if explicit_names.is_empty() && tags.is_empty() && !args.is_empty() {
526 let has_any_symbol = args.iter().any(|a| {
530 let t = a.trim();
531 Self::looks_like_symbol_name(t) || Self::parse_qw_content(t).is_some()
532 });
533 if !has_any_symbol {
534 return (ImportKind::UseEmpty, ImportSymbols::None);
535 }
536 }
537
538 if !tags.is_empty() && explicit_names.is_empty() {
540 return (ImportKind::UseTag, ImportSymbols::Tags(tags));
541 }
542
543 if !tags.is_empty() && !explicit_names.is_empty() {
545 return (
546 ImportKind::UseExplicitList,
547 ImportSymbols::Mixed { tags, names: explicit_names },
548 );
549 }
550
551 if !explicit_names.is_empty() {
553 return (ImportKind::UseExplicitList, ImportSymbols::Explicit(explicit_names));
554 }
555
556 (ImportKind::Use, ImportSymbols::Default)
558 }
559
560 fn classify_use_constant(args: &[String], file_id: FileId, anchor_id: AnchorId) -> ImportSpec {
562 let mut constant_names: Vec<String> = Vec::new();
563
564 if args.is_empty() {
565 return ImportSpec {
567 module: "constant".to_string(),
568 kind: ImportKind::UseConstant,
569 symbols: ImportSymbols::None,
570 provenance: Provenance::ExactAst,
571 confidence: Confidence::High,
572 file_id: Some(file_id),
573 anchor_id: Some(anchor_id),
574 scope_id: None,
575 span_start_byte: None, };
577 }
578
579 if args.first().map(|a| a.as_str()) == Some("{") {
582 let mut i = 1; while i < args.len() {
584 let token = args[i].trim();
585 if token == "}" || token == "=>" || token == "," {
586 i += 1;
587 continue;
588 }
589 if i + 1 < args.len() && args[i + 1].trim() == "=>" {
591 constant_names.push(token.to_string());
592 i += 3;
594 } else {
595 i += 1;
596 }
597 }
598 }
599 else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
601 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
602 constant_names.extend(words);
603 }
604 else if let Some(name) = args.first() {
607 let trimmed = name.trim();
608 if Self::looks_like_constant_name(trimmed) {
609 constant_names.push(trimmed.to_string());
610 }
611 }
612
613 let mut seen = std::collections::HashSet::new();
615 constant_names.retain(|n| seen.insert(n.clone()));
616
617 let symbols = if constant_names.is_empty() {
618 ImportSymbols::None
619 } else {
620 ImportSymbols::Explicit(constant_names)
621 };
622
623 ImportSpec {
624 module: "constant".to_string(),
625 kind: ImportKind::UseConstant,
626 symbols,
627 provenance: Provenance::ExactAst,
628 confidence: Confidence::High,
629 file_id: Some(file_id),
630 anchor_id: Some(anchor_id),
631 scope_id: None,
632 span_start_byte: None, }
634 }
635
636 fn anchor_from_node(node: &Node) -> AnchorId {
640 AnchorId(node.location.start as u64)
643 }
644
645 fn is_version_pragma(module: &str) -> bool {
647 if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
649 return true;
650 }
651 if module.starts_with('v')
653 && module.len() > 1
654 && module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
655 {
656 return true;
657 }
658 false
659 }
660
661 fn parse_qw_content(s: &str) -> Option<&str> {
665 let rest = s.strip_prefix("qw")?;
666 let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
668 Some(inner)
669 }
670
671 fn unquote(s: &str) -> &str {
673 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
674 if s.len() >= 2 {
675 return &s[1..s.len() - 1];
676 }
677 }
678 s
679 }
680
681 fn looks_like_symbol_name(s: &str) -> bool {
683 let s = Self::unquote(s);
684 if s.is_empty() {
685 return false;
686 }
687 if s.starts_with(':') {
689 return true;
690 }
691 if s.starts_with('$')
693 || s.starts_with('@')
694 || s.starts_with('%')
695 || s.starts_with('&')
696 || s.starts_with('*')
697 {
698 return true;
699 }
700 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
702 }
703
704 fn looks_like_constant_name(s: &str) -> bool {
708 if s.is_empty() {
709 return false;
710 }
711 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::Parser;
719
720 fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
722 let mut parser = Parser::new(code);
723 let ast = match parser.parse() {
724 Ok(ast) => ast,
725 Err(_) => return Vec::new(),
726 };
727 ImportExtractor::extract(&ast, FileId(1))
728 }
729
730 #[test]
733 fn test_use_explicit_list_qw() -> Result<(), String> {
734 let specs = parse_and_extract("use List::Util qw(first reduce any);");
735 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
736
737 assert_eq!(spec.module, "List::Util");
738 assert_eq!(spec.kind, ImportKind::UseExplicitList);
739 if let ImportSymbols::Explicit(names) = &spec.symbols {
740 assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
741 assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
742 assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
743 } else {
744 return Err(format!("expected Explicit, got {:?}", spec.symbols));
745 }
746 assert_eq!(spec.file_id, Some(FileId(1)));
747 assert!(spec.anchor_id.is_some());
748 Ok(())
749 }
750
751 #[test]
752 fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
753 let specs = parse_and_extract("use Exporter 'import';");
754 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
755
756 assert_eq!(spec.module, "Exporter");
757 assert_eq!(spec.kind, ImportKind::UseExplicitList);
758 if let ImportSymbols::Explicit(names) = &spec.symbols {
759 assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
760 } else {
761 return Err(format!("expected Explicit, got {:?}", spec.symbols));
762 }
763 Ok(())
764 }
765
766 #[test]
774 fn test_use_empty_parens() -> Result<(), String> {
775 let specs = parse_and_extract("use POSIX ();");
776 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
777
778 assert_eq!(spec.module, "POSIX");
779 assert_eq!(spec.kind, ImportKind::UseEmpty);
783 assert_eq!(spec.symbols, ImportSymbols::None);
784 Ok(())
785 }
786
787 #[test]
790 fn test_use_tag_single() -> Result<(), String> {
791 let specs = parse_and_extract("use POSIX ':sys_wait_h';");
792 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
793
794 assert_eq!(spec.module, "POSIX");
795 assert_eq!(spec.kind, ImportKind::UseTag);
796 if let ImportSymbols::Tags(tags) = &spec.symbols {
797 assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
798 } else {
799 return Err(format!("expected Tags, got {:?}", spec.symbols));
800 }
801 Ok(())
802 }
803
804 #[test]
805 fn test_use_tag_in_qw() -> Result<(), String> {
806 let specs = parse_and_extract("use Fcntl qw(:flock);");
807 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
808
809 assert_eq!(spec.module, "Fcntl");
810 assert_eq!(spec.kind, ImportKind::UseTag);
811 if let ImportSymbols::Tags(tags) = &spec.symbols {
812 assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
813 } else {
814 return Err(format!("expected Tags, got {:?}", spec.symbols));
815 }
816 Ok(())
817 }
818
819 #[test]
822 fn test_use_bare() -> Result<(), String> {
823 let specs = parse_and_extract("use strict;");
824 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
825
826 assert_eq!(spec.module, "strict");
827 assert_eq!(spec.kind, ImportKind::Use);
828 assert_eq!(spec.symbols, ImportSymbols::Default);
829 Ok(())
830 }
831
832 #[test]
833 fn test_use_bare_qualified() -> Result<(), String> {
834 let specs = parse_and_extract("use Data::Dumper;");
835 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
836
837 assert_eq!(spec.module, "Data::Dumper");
838 assert_eq!(spec.kind, ImportKind::Use);
839 assert_eq!(spec.symbols, ImportSymbols::Default);
840 Ok(())
841 }
842
843 #[test]
846 fn test_use_constant_scalar() -> Result<(), String> {
847 let specs = parse_and_extract("use constant PI => 3.14;");
848 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
849
850 assert_eq!(spec.module, "constant");
851 assert_eq!(spec.kind, ImportKind::UseConstant);
852 if let ImportSymbols::Explicit(names) = &spec.symbols {
853 assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
854 } else {
855 return Err(format!("expected Explicit, got {:?}", spec.symbols));
856 }
857 Ok(())
858 }
859
860 #[test]
861 fn test_use_constant_hash_ref() -> Result<(), String> {
862 let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
863 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
864
865 assert_eq!(spec.module, "constant");
866 assert_eq!(spec.kind, ImportKind::UseConstant);
867 if let ImportSymbols::Explicit(names) = &spec.symbols {
868 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
869 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
870 } else {
871 return Err(format!("expected Explicit, got {:?}", spec.symbols));
872 }
873 Ok(())
874 }
875
876 #[test]
877 fn test_use_constant_empty() -> Result<(), String> {
878 let specs = parse_and_extract("use constant;");
879 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
880
881 assert_eq!(spec.module, "constant");
882 assert_eq!(spec.kind, ImportKind::UseConstant);
883 assert_eq!(spec.symbols, ImportSymbols::None);
884 Ok(())
885 }
886
887 #[test]
890 fn test_version_pragma_skipped() -> Result<(), String> {
891 let specs = parse_and_extract("use 5.036;");
892 assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
893 Ok(())
894 }
895
896 #[test]
897 fn test_vstring_pragma_skipped() -> Result<(), String> {
898 let specs = parse_and_extract("use v5.38;");
899 assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
900 Ok(())
901 }
902
903 #[test]
906 fn test_multiple_use_statements() -> Result<(), String> {
907 let code = r#"
908use strict;
909use warnings;
910use List::Util qw(first any);
911use POSIX ();
912use constant MAX => 100;
913"#;
914 let specs = parse_and_extract(code);
915 assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
916
917 assert_eq!(specs[0].module, "strict");
919 assert_eq!(specs[0].kind, ImportKind::Use);
920
921 assert_eq!(specs[1].module, "warnings");
923 assert_eq!(specs[1].kind, ImportKind::Use);
924
925 assert_eq!(specs[2].module, "List::Util");
927 assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
928
929 assert_eq!(specs[3].module, "POSIX");
931 assert_eq!(specs[3].kind, ImportKind::UseEmpty);
932
933 assert_eq!(specs[4].module, "constant");
935 assert_eq!(specs[4].kind, ImportKind::UseConstant);
936
937 Ok(())
938 }
939
940 #[test]
943 fn test_anchor_and_file_id_populated() -> Result<(), String> {
944 let specs = parse_and_extract("use Foo::Bar qw(baz);");
945 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
946
947 assert_eq!(spec.file_id, Some(FileId(1)));
948 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
949 assert_eq!(spec.provenance, Provenance::ExactAst);
950 assert_eq!(spec.confidence, Confidence::High);
951 Ok(())
952 }
953
954 #[test]
957 fn test_use_inside_package_block() -> Result<(), String> {
958 let code = r#"
959package MyModule;
960use Exporter 'import';
961our @EXPORT = qw(foo);
9621;
963"#;
964 let specs = parse_and_extract(code);
965 let exporter_spec =
966 specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
967
968 assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
969 if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
970 assert!(names.contains(&"import".to_string()));
971 } else {
972 return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
973 }
974 Ok(())
975 }
976
977 #[test]
980 fn test_use_mixed_tags_and_names() -> Result<(), String> {
981 let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
982 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
983
984 assert_eq!(spec.module, "Fcntl");
985 assert_eq!(spec.kind, ImportKind::UseExplicitList);
986 if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
987 assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
988 assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
989 assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
990 } else {
991 return Err(format!("expected Mixed, got {:?}", spec.symbols));
992 }
993 Ok(())
994 }
995
996 #[test]
999 fn test_require_bare_module() -> Result<(), String> {
1000 let specs = parse_and_extract("require Foo::Bar;");
1001 let spec = specs
1002 .iter()
1003 .find(|s| s.module == "Foo::Bar")
1004 .ok_or("expected ImportSpec for Foo::Bar")?;
1005
1006 assert_eq!(spec.kind, ImportKind::Require);
1007 assert_eq!(spec.symbols, ImportSymbols::Default);
1008 assert_eq!(spec.provenance, Provenance::ExactAst);
1009 assert_eq!(spec.confidence, Confidence::High);
1010 assert_eq!(spec.file_id, Some(FileId(1)));
1011 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1012 Ok(())
1013 }
1014
1015 #[test]
1018 fn test_require_then_import_with_qw() -> Result<(), String> {
1019 let code = r#"
1020require Foo::Bar;
1021Foo::Bar->import(qw(alpha beta));
1022"#;
1023 let specs = parse_and_extract(code);
1024 let spec = specs
1025 .iter()
1026 .find(|s| s.module == "Foo::Bar")
1027 .ok_or("expected ImportSpec for Foo::Bar")?;
1028
1029 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1030 if let ImportSymbols::Explicit(names) = &spec.symbols {
1031 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
1032 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
1033 } else {
1034 return Err(format!("expected Explicit, got {:?}", spec.symbols));
1035 }
1036 assert_eq!(spec.provenance, Provenance::LiteralRequireImport);
1038 assert_eq!(spec.confidence, Confidence::High);
1039 Ok(())
1040 }
1041
1042 #[test]
1043 fn test_require_then_import_bare() -> Result<(), String> {
1044 let code = r#"
1045require Some::Module;
1046Some::Module->import();
1047"#;
1048 let specs = parse_and_extract(code);
1049 let spec = specs
1050 .iter()
1051 .find(|s| s.module == "Some::Module")
1052 .ok_or("expected ImportSpec for Some::Module")?;
1053
1054 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1055 assert_eq!(spec.symbols, ImportSymbols::Default);
1056 Ok(())
1057 }
1058
1059 #[test]
1060 fn test_require_then_import_quoted_strings() -> Result<(), String> {
1061 let code = r#"
1062require Foo::Bar;
1063Foo::Bar->import('alpha', 'beta');
1064"#;
1065 let specs = parse_and_extract(code);
1066 let spec = specs
1067 .iter()
1068 .find(|s| s.module == "Foo::Bar")
1069 .ok_or("expected ImportSpec for Foo::Bar")?;
1070
1071 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1072 if let ImportSymbols::Explicit(names) = &spec.symbols {
1073 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
1074 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
1075 } else {
1076 return Err(format!("expected Explicit, got {:?}", spec.symbols));
1077 }
1078 assert_eq!(spec.provenance, Provenance::LiteralRequireImport);
1080 assert_eq!(spec.confidence, Confidence::High);
1081 Ok(())
1082 }
1083
1084 #[test]
1085 fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
1086 let code = r#"
1087require Foo::Bar;
1088Foo::Bar->import(@names);
1089"#;
1090 let specs = parse_and_extract(code);
1091 let spec = specs
1092 .iter()
1093 .find(|s| s.module == "Foo::Bar")
1094 .ok_or("expected ImportSpec for Foo::Bar")?;
1095
1096 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1097 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1098 assert_eq!(spec.confidence, Confidence::Low);
1099 Ok(())
1100 }
1101
1102 #[test]
1105 fn test_require_dynamic_variable() -> Result<(), String> {
1106 let specs = parse_and_extract("require $module;");
1107 let spec = specs
1108 .iter()
1109 .find(|s| s.kind == ImportKind::DynamicRequire)
1110 .ok_or("expected DynamicRequire ImportSpec")?;
1111
1112 assert_eq!(spec.module, "");
1113 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1114 assert_eq!(spec.provenance, Provenance::DynamicBoundary);
1117 assert_eq!(spec.confidence, Confidence::Low);
1118 assert_eq!(spec.file_id, Some(FileId(1)));
1119 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1120 Ok(())
1121 }
1122
1123 #[test]
1126 fn test_mixed_use_and_require() -> Result<(), String> {
1127 let code = r#"
1128use strict;
1129use warnings;
1130require Foo::Bar;
1131Foo::Bar->import(qw(baz));
1132require $dynamic;
1133"#;
1134 let specs = parse_and_extract(code);
1135
1136 let strict_spec =
1138 specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
1139 assert_eq!(strict_spec.kind, ImportKind::Use);
1140
1141 let warnings_spec =
1143 specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
1144 assert_eq!(warnings_spec.kind, ImportKind::Use);
1145
1146 let foo_spec =
1148 specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
1149 assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
1150 if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
1151 assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
1152 } else {
1153 return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
1154 }
1155
1156 let dyn_spec = specs
1158 .iter()
1159 .find(|s| s.kind == ImportKind::DynamicRequire)
1160 .ok_or("expected DynamicRequire ImportSpec")?;
1161 assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
1162
1163 Ok(())
1164 }
1165
1166 #[test]
1169 fn test_require_string_path() -> Result<(), String> {
1170 let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
1171 let spec = specs
1172 .iter()
1173 .find(|s| s.module == "Foo::Bar")
1174 .ok_or("expected ImportSpec for Foo::Bar")?;
1175
1176 assert_eq!(spec.kind, ImportKind::Require);
1177 assert_eq!(spec.symbols, ImportSymbols::Default);
1178 Ok(())
1179 }
1180
1181 #[test]
1184 fn standalone_class_dynamic_import_produces_dynamic_spec() -> Result<(), String> {
1185 let specs = parse_and_extract(r#"Foo->import(@names);"#);
1189 let spec = specs
1190 .iter()
1191 .find(|s| s.module == "Foo" && matches!(s.symbols, ImportSymbols::Dynamic))
1192 .ok_or("expected Dynamic ImportSpec for Foo")?;
1193
1194 assert_eq!(spec.provenance, Provenance::DynamicBoundary);
1195 assert_eq!(spec.confidence, Confidence::Low);
1196 assert_eq!(
1197 spec.kind,
1198 ImportKind::ManualImport,
1199 "Class->import(@names) must use ManualImport, not Use"
1200 );
1201 Ok(())
1202 }
1203
1204 #[test]
1205 fn standalone_class_explicit_import_produces_no_dynamic_spec() -> Result<(), String> {
1206 let specs = parse_and_extract(r#"Foo->import('bar');"#);
1209 let dynamic_specs: Vec<_> =
1210 specs.iter().filter(|s| matches!(s.symbols, ImportSymbols::Dynamic)).collect();
1211
1212 assert!(dynamic_specs.is_empty(), "explicit import args must not produce a Dynamic spec");
1213 Ok(())
1214 }
1215
1216 #[test]
1217 fn variable_class_import_does_not_produce_standalone_spec() -> Result<(), String> {
1218 let specs = parse_and_extract(r#"$var->import(@names);"#);
1221 let standalone_dynamic: Vec<_> = specs
1224 .iter()
1225 .filter(|s| matches!(s.symbols, ImportSymbols::Dynamic) && s.module.is_empty())
1226 .collect();
1227
1228 assert!(
1231 standalone_dynamic.is_empty(),
1232 "variable-class import without require must not produce standalone Dynamic spec"
1233 );
1234 Ok(())
1235 }
1236}