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 {
201 let anchor_id = Self::anchor_from_node(node);
202 ImportSpec {
203 module: String::new(),
204 kind: ImportKind::DynamicRequire,
205 symbols: ImportSymbols::Dynamic,
206 provenance: Provenance::ExactAst,
207 confidence: Confidence::Low,
208 file_id: Some(file_id),
209 anchor_id: Some(anchor_id),
210 scope_id: None,
211 }
212 }
213
214 fn try_match_import_call<'a>(
219 node: &'a Node,
220 expected_module: &str,
221 ) -> Option<(ImportSymbols, &'a Node)> {
222 let (object, method, args) = match &node.kind {
223 NodeKind::MethodCall { object, method, args } => (object, method, args),
224 _ => return None,
225 };
226
227 if method != "import" {
228 return None;
229 }
230
231 let obj_name = match &object.kind {
233 NodeKind::Identifier { name } => name.as_str(),
234 _ => return None,
235 };
236
237 if obj_name != expected_module {
238 return None;
239 }
240
241 let symbols = Self::extract_import_call_symbols(args);
243 Some((symbols, node))
244 }
245
246 fn extract_import_call_symbols(args: &[Node]) -> ImportSymbols {
248 if args.is_empty() {
249 return ImportSymbols::Default;
250 }
251
252 let mut names: Vec<String> = Vec::new();
253 let mut tags: Vec<String> = Vec::new();
254 let mut has_dynamic_arg = false;
255
256 for arg in args {
257 has_dynamic_arg |= Self::collect_import_arg_symbols(arg, &mut names, &mut tags);
258 }
259
260 if has_dynamic_arg {
261 return ImportSymbols::Dynamic;
262 }
263
264 if names.is_empty() && tags.is_empty() {
265 return ImportSymbols::Default;
266 }
267
268 if !tags.is_empty() && names.is_empty() {
269 return ImportSymbols::Tags(tags);
270 }
271
272 if !tags.is_empty() && !names.is_empty() {
273 return ImportSymbols::Mixed { tags, names };
274 }
275
276 ImportSymbols::Explicit(names)
277 }
278
279 fn collect_import_arg_symbols(
285 arg: &Node,
286 names: &mut Vec<String>,
287 tags: &mut Vec<String>,
288 ) -> bool {
289 match &arg.kind {
290 NodeKind::String { value, .. } => {
291 let bare = value.trim_matches('\'').trim_matches('"');
292 if let Some(tag) = bare.strip_prefix(':') {
293 tags.push(tag.to_string());
294 } else if !bare.is_empty() {
295 names.push(bare.to_string());
296 }
297 false
298 }
299 NodeKind::Identifier { name } => {
300 if let Some(inner) = Self::parse_qw_content(name) {
302 for word in inner.split_whitespace() {
303 if let Some(tag) = word.strip_prefix(':') {
304 tags.push(tag.to_string());
305 } else {
306 names.push(word.to_string());
307 }
308 }
309 } else if let Some(tag) = name.strip_prefix(':') {
310 tags.push(tag.to_string());
311 } else if !name.is_empty() {
312 names.push(name.clone());
313 }
314 false
315 }
316 NodeKind::Variable { .. } => {
317 true
320 }
321 NodeKind::ArrayLiteral { elements } => {
322 let mut has_dynamic_arg = false;
324 for el in elements {
325 has_dynamic_arg |= Self::collect_import_arg_symbols(el, names, tags);
326 }
327 has_dynamic_arg
328 }
329 _ => true,
330 }
331 }
332
333 fn confidence_for_symbols(symbols: &ImportSymbols) -> Confidence {
334 if matches!(symbols, ImportSymbols::Dynamic) { Confidence::Low } else { Confidence::High }
335 }
336
337 fn classify_use(
344 module: &str,
345 args: &[String],
346 file_id: FileId,
347 node: &Node,
348 ) -> Option<ImportSpec> {
349 if Self::is_version_pragma(module) {
351 return None;
352 }
353
354 let anchor_id = Self::anchor_from_node(node);
355
356 if module == "constant" {
358 return Some(Self::classify_use_constant(args, file_id, anchor_id));
359 }
360
361 let (kind, symbols) = Self::classify_args(args, module, node);
363
364 Some(ImportSpec {
365 module: module.to_string(),
366 kind,
367 symbols,
368 provenance: Provenance::ExactAst,
369 confidence: Confidence::High,
370 file_id: Some(file_id),
371 anchor_id: Some(anchor_id),
372 scope_id: None,
373 })
374 }
375
376 fn classify_args(args: &[String], module: &str, node: &Node) -> (ImportKind, ImportSymbols) {
378 if args.is_empty() {
379 let bare_len = "use ".len() + module.len() + 1; let span_len = node.location.end.saturating_sub(node.location.start);
386 if span_len > bare_len {
387 return (ImportKind::UseEmpty, ImportSymbols::None);
390 }
391 return (ImportKind::Use, ImportSymbols::Default);
393 }
394
395 let mut explicit_names: Vec<String> = Vec::new();
397 let mut tags: Vec<String> = Vec::new();
398
399 for arg in args {
400 let trimmed = arg.trim();
401
402 if let Some(inner) = Self::parse_qw_content(trimmed) {
404 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
405 for word in words {
406 if let Some(tag) = word.strip_prefix(':') {
407 tags.push(tag.to_string());
408 } else {
409 explicit_names.push(word);
410 }
411 }
412 continue;
413 }
414
415 let unquoted = Self::unquote(trimmed);
417 if let Some(tag) = unquoted.strip_prefix(':') {
418 tags.push(tag.to_string());
419 continue;
420 }
421
422 if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
425 continue;
426 }
427
428 if Self::looks_like_symbol_name(trimmed) {
430 explicit_names.push(Self::unquote(trimmed).to_string());
431 }
432 }
433
434 if explicit_names.is_empty() && tags.is_empty() && !args.is_empty() {
436 let has_any_symbol = args.iter().any(|a| {
440 let t = a.trim();
441 Self::looks_like_symbol_name(t) || Self::parse_qw_content(t).is_some()
442 });
443 if !has_any_symbol {
444 return (ImportKind::UseEmpty, ImportSymbols::None);
445 }
446 }
447
448 if !tags.is_empty() && explicit_names.is_empty() {
450 return (ImportKind::UseTag, ImportSymbols::Tags(tags));
451 }
452
453 if !tags.is_empty() && !explicit_names.is_empty() {
455 return (
456 ImportKind::UseExplicitList,
457 ImportSymbols::Mixed { tags, names: explicit_names },
458 );
459 }
460
461 if !explicit_names.is_empty() {
463 return (ImportKind::UseExplicitList, ImportSymbols::Explicit(explicit_names));
464 }
465
466 (ImportKind::Use, ImportSymbols::Default)
468 }
469
470 fn classify_use_constant(args: &[String], file_id: FileId, anchor_id: AnchorId) -> ImportSpec {
472 let mut constant_names: Vec<String> = Vec::new();
473
474 if args.is_empty() {
475 return ImportSpec {
477 module: "constant".to_string(),
478 kind: ImportKind::UseConstant,
479 symbols: ImportSymbols::None,
480 provenance: Provenance::ExactAst,
481 confidence: Confidence::High,
482 file_id: Some(file_id),
483 anchor_id: Some(anchor_id),
484 scope_id: None,
485 };
486 }
487
488 if args.first().map(|a| a.as_str()) == Some("{") {
491 let mut i = 1; while i < args.len() {
493 let token = args[i].trim();
494 if token == "}" || token == "=>" || token == "," {
495 i += 1;
496 continue;
497 }
498 if i + 1 < args.len() && args[i + 1].trim() == "=>" {
500 constant_names.push(token.to_string());
501 i += 3;
503 } else {
504 i += 1;
505 }
506 }
507 }
508 else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
510 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
511 constant_names.extend(words);
512 }
513 else if let Some(name) = args.first() {
516 let trimmed = name.trim();
517 if Self::looks_like_constant_name(trimmed) {
518 constant_names.push(trimmed.to_string());
519 }
520 }
521
522 let mut seen = std::collections::HashSet::new();
524 constant_names.retain(|n| seen.insert(n.clone()));
525
526 let symbols = if constant_names.is_empty() {
527 ImportSymbols::None
528 } else {
529 ImportSymbols::Explicit(constant_names)
530 };
531
532 ImportSpec {
533 module: "constant".to_string(),
534 kind: ImportKind::UseConstant,
535 symbols,
536 provenance: Provenance::ExactAst,
537 confidence: Confidence::High,
538 file_id: Some(file_id),
539 anchor_id: Some(anchor_id),
540 scope_id: None,
541 }
542 }
543
544 fn anchor_from_node(node: &Node) -> AnchorId {
548 AnchorId(node.location.start as u64)
551 }
552
553 fn is_version_pragma(module: &str) -> bool {
555 if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
557 return true;
558 }
559 if module.starts_with('v')
561 && module.len() > 1
562 && module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
563 {
564 return true;
565 }
566 false
567 }
568
569 fn parse_qw_content(s: &str) -> Option<&str> {
573 let rest = s.strip_prefix("qw")?;
574 let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
576 Some(inner)
577 }
578
579 fn unquote(s: &str) -> &str {
581 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
582 if s.len() >= 2 {
583 return &s[1..s.len() - 1];
584 }
585 }
586 s
587 }
588
589 fn looks_like_symbol_name(s: &str) -> bool {
591 let s = Self::unquote(s);
592 if s.is_empty() {
593 return false;
594 }
595 if s.starts_with(':') {
597 return true;
598 }
599 if s.starts_with('$')
601 || s.starts_with('@')
602 || s.starts_with('%')
603 || s.starts_with('&')
604 || s.starts_with('*')
605 {
606 return true;
607 }
608 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
610 }
611
612 fn looks_like_constant_name(s: &str) -> bool {
616 if s.is_empty() {
617 return false;
618 }
619 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::Parser;
627
628 fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
630 let mut parser = Parser::new(code);
631 let ast = match parser.parse() {
632 Ok(ast) => ast,
633 Err(_) => return Vec::new(),
634 };
635 ImportExtractor::extract(&ast, FileId(1))
636 }
637
638 #[test]
641 fn test_use_explicit_list_qw() -> Result<(), String> {
642 let specs = parse_and_extract("use List::Util qw(first reduce any);");
643 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
644
645 assert_eq!(spec.module, "List::Util");
646 assert_eq!(spec.kind, ImportKind::UseExplicitList);
647 if let ImportSymbols::Explicit(names) = &spec.symbols {
648 assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
649 assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
650 assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
651 } else {
652 return Err(format!("expected Explicit, got {:?}", spec.symbols));
653 }
654 assert_eq!(spec.file_id, Some(FileId(1)));
655 assert!(spec.anchor_id.is_some());
656 Ok(())
657 }
658
659 #[test]
660 fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
661 let specs = parse_and_extract("use Exporter 'import';");
662 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
663
664 assert_eq!(spec.module, "Exporter");
665 assert_eq!(spec.kind, ImportKind::UseExplicitList);
666 if let ImportSymbols::Explicit(names) = &spec.symbols {
667 assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
668 } else {
669 return Err(format!("expected Explicit, got {:?}", spec.symbols));
670 }
671 Ok(())
672 }
673
674 #[test]
682 fn test_use_empty_parens() -> Result<(), String> {
683 let specs = parse_and_extract("use POSIX ();");
684 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
685
686 assert_eq!(spec.module, "POSIX");
687 assert_eq!(spec.kind, ImportKind::UseEmpty);
691 assert_eq!(spec.symbols, ImportSymbols::None);
692 Ok(())
693 }
694
695 #[test]
698 fn test_use_tag_single() -> Result<(), String> {
699 let specs = parse_and_extract("use POSIX ':sys_wait_h';");
700 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
701
702 assert_eq!(spec.module, "POSIX");
703 assert_eq!(spec.kind, ImportKind::UseTag);
704 if let ImportSymbols::Tags(tags) = &spec.symbols {
705 assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
706 } else {
707 return Err(format!("expected Tags, got {:?}", spec.symbols));
708 }
709 Ok(())
710 }
711
712 #[test]
713 fn test_use_tag_in_qw() -> Result<(), String> {
714 let specs = parse_and_extract("use Fcntl qw(:flock);");
715 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
716
717 assert_eq!(spec.module, "Fcntl");
718 assert_eq!(spec.kind, ImportKind::UseTag);
719 if let ImportSymbols::Tags(tags) = &spec.symbols {
720 assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
721 } else {
722 return Err(format!("expected Tags, got {:?}", spec.symbols));
723 }
724 Ok(())
725 }
726
727 #[test]
730 fn test_use_bare() -> Result<(), String> {
731 let specs = parse_and_extract("use strict;");
732 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
733
734 assert_eq!(spec.module, "strict");
735 assert_eq!(spec.kind, ImportKind::Use);
736 assert_eq!(spec.symbols, ImportSymbols::Default);
737 Ok(())
738 }
739
740 #[test]
741 fn test_use_bare_qualified() -> Result<(), String> {
742 let specs = parse_and_extract("use Data::Dumper;");
743 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
744
745 assert_eq!(spec.module, "Data::Dumper");
746 assert_eq!(spec.kind, ImportKind::Use);
747 assert_eq!(spec.symbols, ImportSymbols::Default);
748 Ok(())
749 }
750
751 #[test]
754 fn test_use_constant_scalar() -> Result<(), String> {
755 let specs = parse_and_extract("use constant PI => 3.14;");
756 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
757
758 assert_eq!(spec.module, "constant");
759 assert_eq!(spec.kind, ImportKind::UseConstant);
760 if let ImportSymbols::Explicit(names) = &spec.symbols {
761 assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
762 } else {
763 return Err(format!("expected Explicit, got {:?}", spec.symbols));
764 }
765 Ok(())
766 }
767
768 #[test]
769 fn test_use_constant_hash_ref() -> Result<(), String> {
770 let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
771 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
772
773 assert_eq!(spec.module, "constant");
774 assert_eq!(spec.kind, ImportKind::UseConstant);
775 if let ImportSymbols::Explicit(names) = &spec.symbols {
776 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
777 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
778 } else {
779 return Err(format!("expected Explicit, got {:?}", spec.symbols));
780 }
781 Ok(())
782 }
783
784 #[test]
785 fn test_use_constant_empty() -> Result<(), String> {
786 let specs = parse_and_extract("use constant;");
787 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
788
789 assert_eq!(spec.module, "constant");
790 assert_eq!(spec.kind, ImportKind::UseConstant);
791 assert_eq!(spec.symbols, ImportSymbols::None);
792 Ok(())
793 }
794
795 #[test]
798 fn test_version_pragma_skipped() -> Result<(), String> {
799 let specs = parse_and_extract("use 5.036;");
800 assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
801 Ok(())
802 }
803
804 #[test]
805 fn test_vstring_pragma_skipped() -> Result<(), String> {
806 let specs = parse_and_extract("use v5.38;");
807 assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
808 Ok(())
809 }
810
811 #[test]
814 fn test_multiple_use_statements() -> Result<(), String> {
815 let code = r#"
816use strict;
817use warnings;
818use List::Util qw(first any);
819use POSIX ();
820use constant MAX => 100;
821"#;
822 let specs = parse_and_extract(code);
823 assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
824
825 assert_eq!(specs[0].module, "strict");
827 assert_eq!(specs[0].kind, ImportKind::Use);
828
829 assert_eq!(specs[1].module, "warnings");
831 assert_eq!(specs[1].kind, ImportKind::Use);
832
833 assert_eq!(specs[2].module, "List::Util");
835 assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
836
837 assert_eq!(specs[3].module, "POSIX");
839 assert_eq!(specs[3].kind, ImportKind::UseEmpty);
840
841 assert_eq!(specs[4].module, "constant");
843 assert_eq!(specs[4].kind, ImportKind::UseConstant);
844
845 Ok(())
846 }
847
848 #[test]
851 fn test_anchor_and_file_id_populated() -> Result<(), String> {
852 let specs = parse_and_extract("use Foo::Bar qw(baz);");
853 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
854
855 assert_eq!(spec.file_id, Some(FileId(1)));
856 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
857 assert_eq!(spec.provenance, Provenance::ExactAst);
858 assert_eq!(spec.confidence, Confidence::High);
859 Ok(())
860 }
861
862 #[test]
865 fn test_use_inside_package_block() -> Result<(), String> {
866 let code = r#"
867package MyModule;
868use Exporter 'import';
869our @EXPORT = qw(foo);
8701;
871"#;
872 let specs = parse_and_extract(code);
873 let exporter_spec =
874 specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
875
876 assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
877 if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
878 assert!(names.contains(&"import".to_string()));
879 } else {
880 return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
881 }
882 Ok(())
883 }
884
885 #[test]
888 fn test_use_mixed_tags_and_names() -> Result<(), String> {
889 let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
890 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
891
892 assert_eq!(spec.module, "Fcntl");
893 assert_eq!(spec.kind, ImportKind::UseExplicitList);
894 if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
895 assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
896 assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
897 assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
898 } else {
899 return Err(format!("expected Mixed, got {:?}", spec.symbols));
900 }
901 Ok(())
902 }
903
904 #[test]
907 fn test_require_bare_module() -> Result<(), String> {
908 let specs = parse_and_extract("require Foo::Bar;");
909 let spec = specs
910 .iter()
911 .find(|s| s.module == "Foo::Bar")
912 .ok_or("expected ImportSpec for Foo::Bar")?;
913
914 assert_eq!(spec.kind, ImportKind::Require);
915 assert_eq!(spec.symbols, ImportSymbols::Default);
916 assert_eq!(spec.provenance, Provenance::ExactAst);
917 assert_eq!(spec.confidence, Confidence::High);
918 assert_eq!(spec.file_id, Some(FileId(1)));
919 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
920 Ok(())
921 }
922
923 #[test]
926 fn test_require_then_import_with_qw() -> Result<(), String> {
927 let code = r#"
928require Foo::Bar;
929Foo::Bar->import(qw(alpha beta));
930"#;
931 let specs = parse_and_extract(code);
932 let spec = specs
933 .iter()
934 .find(|s| s.module == "Foo::Bar")
935 .ok_or("expected ImportSpec for Foo::Bar")?;
936
937 assert_eq!(spec.kind, ImportKind::RequireThenImport);
938 if let ImportSymbols::Explicit(names) = &spec.symbols {
939 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
940 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
941 } else {
942 return Err(format!("expected Explicit, got {:?}", spec.symbols));
943 }
944 assert_eq!(spec.provenance, Provenance::ExactAst);
945 assert_eq!(spec.confidence, Confidence::High);
946 Ok(())
947 }
948
949 #[test]
950 fn test_require_then_import_bare() -> Result<(), String> {
951 let code = r#"
952require Some::Module;
953Some::Module->import();
954"#;
955 let specs = parse_and_extract(code);
956 let spec = specs
957 .iter()
958 .find(|s| s.module == "Some::Module")
959 .ok_or("expected ImportSpec for Some::Module")?;
960
961 assert_eq!(spec.kind, ImportKind::RequireThenImport);
962 assert_eq!(spec.symbols, ImportSymbols::Default);
963 Ok(())
964 }
965
966 #[test]
967 fn test_require_then_import_quoted_strings() -> Result<(), String> {
968 let code = r#"
969require Foo::Bar;
970Foo::Bar->import('alpha', 'beta');
971"#;
972 let specs = parse_and_extract(code);
973 let spec = specs
974 .iter()
975 .find(|s| s.module == "Foo::Bar")
976 .ok_or("expected ImportSpec for Foo::Bar")?;
977
978 assert_eq!(spec.kind, ImportKind::RequireThenImport);
979 if let ImportSymbols::Explicit(names) = &spec.symbols {
980 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
981 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
982 } else {
983 return Err(format!("expected Explicit, got {:?}", spec.symbols));
984 }
985 assert_eq!(spec.confidence, Confidence::High);
986 Ok(())
987 }
988
989 #[test]
990 fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
991 let code = r#"
992require Foo::Bar;
993Foo::Bar->import(@names);
994"#;
995 let specs = parse_and_extract(code);
996 let spec = specs
997 .iter()
998 .find(|s| s.module == "Foo::Bar")
999 .ok_or("expected ImportSpec for Foo::Bar")?;
1000
1001 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1002 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1003 assert_eq!(spec.confidence, Confidence::Low);
1004 Ok(())
1005 }
1006
1007 #[test]
1010 fn test_require_dynamic_variable() -> Result<(), String> {
1011 let specs = parse_and_extract("require $module;");
1012 let spec = specs
1013 .iter()
1014 .find(|s| s.kind == ImportKind::DynamicRequire)
1015 .ok_or("expected DynamicRequire ImportSpec")?;
1016
1017 assert_eq!(spec.module, "");
1018 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1019 assert_eq!(spec.provenance, Provenance::ExactAst);
1020 assert_eq!(spec.confidence, Confidence::Low);
1021 assert_eq!(spec.file_id, Some(FileId(1)));
1022 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1023 Ok(())
1024 }
1025
1026 #[test]
1029 fn test_mixed_use_and_require() -> Result<(), String> {
1030 let code = r#"
1031use strict;
1032use warnings;
1033require Foo::Bar;
1034Foo::Bar->import(qw(baz));
1035require $dynamic;
1036"#;
1037 let specs = parse_and_extract(code);
1038
1039 let strict_spec =
1041 specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
1042 assert_eq!(strict_spec.kind, ImportKind::Use);
1043
1044 let warnings_spec =
1046 specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
1047 assert_eq!(warnings_spec.kind, ImportKind::Use);
1048
1049 let foo_spec =
1051 specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
1052 assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
1053 if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
1054 assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
1055 } else {
1056 return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
1057 }
1058
1059 let dyn_spec = specs
1061 .iter()
1062 .find(|s| s.kind == ImportKind::DynamicRequire)
1063 .ok_or("expected DynamicRequire ImportSpec")?;
1064 assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
1065
1066 Ok(())
1067 }
1068
1069 #[test]
1072 fn test_require_string_path() -> Result<(), String> {
1073 let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
1074 let spec = specs
1075 .iter()
1076 .find(|s| s.module == "Foo::Bar")
1077 .ok_or("expected ImportSpec for Foo::Bar")?;
1078
1079 assert_eq!(spec.kind, ImportKind::Require);
1080 assert_eq!(spec.symbols, ImportSymbols::Default);
1081 Ok(())
1082 }
1083}