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 let starts_hash_form = args.first().map(|a| a.as_str()) == Some("{")
582 || args.first().map(|a| a.as_str()) == Some("+{")
583 || (args.first().map(|a| a.as_str()) == Some("+")
584 && args.get(1).map(|a| a.as_str()) == Some("{"));
585 if starts_hash_form {
586 let mut i = 0;
587 while i < args.len() {
588 let token = args[i].trim();
589 if Self::is_constant_hash_punctuation(token) {
590 i += 1;
591 continue;
592 }
593 if i + 1 < args.len()
594 && args[i + 1].trim() == "=>"
595 && let Some(name) = Self::constant_name_candidate(token)
596 {
597 constant_names.push(name);
598 i += 2;
599 let mut nesting = 0usize;
600 while i < args.len() {
601 let value_token = args[i].trim();
602 if nesting == 0 && (value_token == "," || value_token == "}") {
603 break;
604 }
605 match value_token {
606 "{" | "[" | "(" => nesting += 1,
607 "}" | "]" | ")" if nesting > 0 => nesting -= 1,
608 "}" if nesting == 0 => break,
609 _ => {}
610 }
611 i += 1;
612 }
613 } else {
614 i += 1;
615 }
616 }
617 }
618 else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
620 let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
621 constant_names.extend(words);
622 }
623 else if let Some(name) = args.first() {
626 let trimmed = name.trim();
627 if let Some(name) = Self::constant_name_candidate(trimmed) {
628 constant_names.push(name);
629 }
630 }
631
632 let mut seen = std::collections::HashSet::new();
634 constant_names.retain(|n| seen.insert(n.clone()));
635
636 let symbols = if constant_names.is_empty() {
637 ImportSymbols::None
638 } else {
639 ImportSymbols::Explicit(constant_names)
640 };
641
642 ImportSpec {
643 module: "constant".to_string(),
644 kind: ImportKind::UseConstant,
645 symbols,
646 provenance: Provenance::ExactAst,
647 confidence: Confidence::High,
648 file_id: Some(file_id),
649 anchor_id: Some(anchor_id),
650 scope_id: None,
651 span_start_byte: None, }
653 }
654
655 fn anchor_from_node(node: &Node) -> AnchorId {
659 AnchorId(node.location.start as u64)
662 }
663
664 fn is_version_pragma(module: &str) -> bool {
666 if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
668 return true;
669 }
670 if module.starts_with('v')
672 && module.len() > 1
673 && module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
674 {
675 return true;
676 }
677 false
678 }
679
680 fn parse_qw_content(s: &str) -> Option<&str> {
684 let rest = s.strip_prefix("qw")?;
685 let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
687 Some(inner)
688 }
689
690 fn unquote(s: &str) -> &str {
692 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
693 if s.len() >= 2 {
694 return &s[1..s.len() - 1];
695 }
696 }
697 s
698 }
699
700 fn looks_like_symbol_name(s: &str) -> bool {
702 let s = Self::unquote(s);
703 if s.is_empty() {
704 return false;
705 }
706 if s.starts_with(':') {
708 return true;
709 }
710 if s.starts_with('$')
712 || s.starts_with('@')
713 || s.starts_with('%')
714 || s.starts_with('&')
715 || s.starts_with('*')
716 {
717 return true;
718 }
719 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
721 }
722
723 fn looks_like_constant_name(s: &str) -> bool {
727 if s.is_empty() {
728 return false;
729 }
730 s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
731 }
732
733 fn constant_name_candidate(s: &str) -> Option<String> {
734 let name = Self::unquote(s.trim());
735 Self::looks_like_constant_name(name).then(|| name.to_string())
736 }
737
738 fn is_constant_hash_punctuation(s: &str) -> bool {
739 matches!(s, "+" | "+{" | "{" | "}" | "=>" | ",")
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use crate::Parser;
747
748 fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
750 let mut parser = Parser::new(code);
751 let ast = match parser.parse() {
752 Ok(ast) => ast,
753 Err(_) => return Vec::new(),
754 };
755 ImportExtractor::extract(&ast, FileId(1))
756 }
757
758 #[test]
761 fn test_use_explicit_list_qw() -> Result<(), String> {
762 let specs = parse_and_extract("use List::Util qw(first reduce any);");
763 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
764
765 assert_eq!(spec.module, "List::Util");
766 assert_eq!(spec.kind, ImportKind::UseExplicitList);
767 if let ImportSymbols::Explicit(names) = &spec.symbols {
768 assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
769 assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
770 assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
771 } else {
772 return Err(format!("expected Explicit, got {:?}", spec.symbols));
773 }
774 assert_eq!(spec.file_id, Some(FileId(1)));
775 assert!(spec.anchor_id.is_some());
776 Ok(())
777 }
778
779 #[test]
780 fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
781 let specs = parse_and_extract("use Exporter 'import';");
782 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
783
784 assert_eq!(spec.module, "Exporter");
785 assert_eq!(spec.kind, ImportKind::UseExplicitList);
786 if let ImportSymbols::Explicit(names) = &spec.symbols {
787 assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
788 } else {
789 return Err(format!("expected Explicit, got {:?}", spec.symbols));
790 }
791 Ok(())
792 }
793
794 #[test]
802 fn test_use_empty_parens() -> Result<(), String> {
803 let specs = parse_and_extract("use POSIX ();");
804 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
805
806 assert_eq!(spec.module, "POSIX");
807 assert_eq!(spec.kind, ImportKind::UseEmpty);
811 assert_eq!(spec.symbols, ImportSymbols::None);
812 Ok(())
813 }
814
815 #[test]
818 fn test_use_tag_single() -> Result<(), String> {
819 let specs = parse_and_extract("use POSIX ':sys_wait_h';");
820 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
821
822 assert_eq!(spec.module, "POSIX");
823 assert_eq!(spec.kind, ImportKind::UseTag);
824 if let ImportSymbols::Tags(tags) = &spec.symbols {
825 assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
826 } else {
827 return Err(format!("expected Tags, got {:?}", spec.symbols));
828 }
829 Ok(())
830 }
831
832 #[test]
833 fn test_use_tag_in_qw() -> Result<(), String> {
834 let specs = parse_and_extract("use Fcntl qw(:flock);");
835 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
836
837 assert_eq!(spec.module, "Fcntl");
838 assert_eq!(spec.kind, ImportKind::UseTag);
839 if let ImportSymbols::Tags(tags) = &spec.symbols {
840 assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
841 } else {
842 return Err(format!("expected Tags, got {:?}", spec.symbols));
843 }
844 Ok(())
845 }
846
847 #[test]
850 fn test_use_bare() -> Result<(), String> {
851 let specs = parse_and_extract("use strict;");
852 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
853
854 assert_eq!(spec.module, "strict");
855 assert_eq!(spec.kind, ImportKind::Use);
856 assert_eq!(spec.symbols, ImportSymbols::Default);
857 Ok(())
858 }
859
860 #[test]
861 fn test_use_bare_qualified() -> Result<(), String> {
862 let specs = parse_and_extract("use Data::Dumper;");
863 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
864
865 assert_eq!(spec.module, "Data::Dumper");
866 assert_eq!(spec.kind, ImportKind::Use);
867 assert_eq!(spec.symbols, ImportSymbols::Default);
868 Ok(())
869 }
870
871 #[test]
874 fn test_use_constant_scalar() -> Result<(), String> {
875 let specs = parse_and_extract("use constant PI => 3.14;");
876 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
877
878 assert_eq!(spec.module, "constant");
879 assert_eq!(spec.kind, ImportKind::UseConstant);
880 if let ImportSymbols::Explicit(names) = &spec.symbols {
881 assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
882 } else {
883 return Err(format!("expected Explicit, got {:?}", spec.symbols));
884 }
885 Ok(())
886 }
887
888 #[test]
889 fn test_use_constant_hash_ref() -> Result<(), String> {
890 let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
891 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
892
893 assert_eq!(spec.module, "constant");
894 assert_eq!(spec.kind, ImportKind::UseConstant);
895 if let ImportSymbols::Explicit(names) = &spec.symbols {
896 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
897 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
898 } else {
899 return Err(format!("expected Explicit, got {:?}", spec.symbols));
900 }
901 Ok(())
902 }
903
904 #[test]
905 fn test_use_constant_quoted_scalar() -> Result<(), String> {
906 let specs = parse_and_extract("use constant 'HTTP_OK' => 200;");
907 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
908
909 assert_eq!(spec.module, "constant");
910 assert_eq!(spec.kind, ImportKind::UseConstant);
911 if let ImportSymbols::Explicit(names) = &spec.symbols {
912 assert!(names.contains(&"HTTP_OK".to_string()), "missing 'HTTP_OK' in {names:?}");
913 } else {
914 return Err(format!("expected Explicit, got {:?}", spec.symbols));
915 }
916 Ok(())
917 }
918
919 #[test]
920 fn test_use_constant_quoted_hash_ref() -> Result<(), String> {
921 let specs = parse_and_extract(r#"use constant { 'FOO' => 1, "BAR" => 2 };"#);
922 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
923
924 assert_eq!(spec.module, "constant");
925 assert_eq!(spec.kind, ImportKind::UseConstant);
926 if let ImportSymbols::Explicit(names) = &spec.symbols {
927 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
928 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
929 } else {
930 return Err(format!("expected Explicit, got {:?}", spec.symbols));
931 }
932 Ok(())
933 }
934
935 #[test]
936 fn test_use_constant_plus_hash_ref() -> Result<(), String> {
937 let specs = parse_and_extract("use constant +{ FOO => 1, BAR => 2 };");
938 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
939
940 assert_eq!(spec.module, "constant");
941 assert_eq!(spec.kind, ImportKind::UseConstant);
942 if let ImportSymbols::Explicit(names) = &spec.symbols {
943 assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
944 assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
945 } else {
946 return Err(format!("expected Explicit, got {:?}", spec.symbols));
947 }
948 Ok(())
949 }
950
951 #[test]
952 fn test_use_constant_hash_ref_ignores_nested_fat_comma_values() -> Result<(), String> {
953 let specs = parse_and_extract("use constant { FOO => { nested => 1 }, BAR => 2 };");
954 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
955
956 assert_eq!(spec.module, "constant");
957 assert_eq!(spec.kind, ImportKind::UseConstant);
958 if let ImportSymbols::Explicit(names) = &spec.symbols {
959 assert_eq!(names, &vec!["FOO".to_string(), "BAR".to_string()]);
960 } else {
961 return Err(format!("expected Explicit, got {:?}", spec.symbols));
962 }
963 Ok(())
964 }
965
966 #[test]
967 fn test_use_constant_empty() -> Result<(), String> {
968 let specs = parse_and_extract("use constant;");
969 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
970
971 assert_eq!(spec.module, "constant");
972 assert_eq!(spec.kind, ImportKind::UseConstant);
973 assert_eq!(spec.symbols, ImportSymbols::None);
974 Ok(())
975 }
976
977 #[test]
980 fn test_version_pragma_skipped() -> Result<(), String> {
981 let specs = parse_and_extract("use 5.036;");
982 assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
983 Ok(())
984 }
985
986 #[test]
987 fn test_vstring_pragma_skipped() -> Result<(), String> {
988 let specs = parse_and_extract("use v5.38;");
989 assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
990 Ok(())
991 }
992
993 #[test]
996 fn test_multiple_use_statements() -> Result<(), String> {
997 let code = r#"
998use strict;
999use warnings;
1000use List::Util qw(first any);
1001use POSIX ();
1002use constant MAX => 100;
1003"#;
1004 let specs = parse_and_extract(code);
1005 assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
1006
1007 assert_eq!(specs[0].module, "strict");
1009 assert_eq!(specs[0].kind, ImportKind::Use);
1010
1011 assert_eq!(specs[1].module, "warnings");
1013 assert_eq!(specs[1].kind, ImportKind::Use);
1014
1015 assert_eq!(specs[2].module, "List::Util");
1017 assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
1018
1019 assert_eq!(specs[3].module, "POSIX");
1021 assert_eq!(specs[3].kind, ImportKind::UseEmpty);
1022
1023 assert_eq!(specs[4].module, "constant");
1025 assert_eq!(specs[4].kind, ImportKind::UseConstant);
1026
1027 Ok(())
1028 }
1029
1030 #[test]
1033 fn test_anchor_and_file_id_populated() -> Result<(), String> {
1034 let specs = parse_and_extract("use Foo::Bar qw(baz);");
1035 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
1036
1037 assert_eq!(spec.file_id, Some(FileId(1)));
1038 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1039 assert_eq!(spec.provenance, Provenance::ExactAst);
1040 assert_eq!(spec.confidence, Confidence::High);
1041 Ok(())
1042 }
1043
1044 #[test]
1047 fn test_use_inside_package_block() -> Result<(), String> {
1048 let code = r#"
1049package MyModule;
1050use Exporter 'import';
1051our @EXPORT = qw(foo);
10521;
1053"#;
1054 let specs = parse_and_extract(code);
1055 let exporter_spec =
1056 specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
1057
1058 assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
1059 if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
1060 assert!(names.contains(&"import".to_string()));
1061 } else {
1062 return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
1063 }
1064 Ok(())
1065 }
1066
1067 #[test]
1070 fn test_use_mixed_tags_and_names() -> Result<(), String> {
1071 let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
1072 let spec = specs.first().ok_or("expected at least one ImportSpec")?;
1073
1074 assert_eq!(spec.module, "Fcntl");
1075 assert_eq!(spec.kind, ImportKind::UseExplicitList);
1076 if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
1077 assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
1078 assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
1079 assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
1080 } else {
1081 return Err(format!("expected Mixed, got {:?}", spec.symbols));
1082 }
1083 Ok(())
1084 }
1085
1086 #[test]
1089 fn test_require_bare_module() -> Result<(), String> {
1090 let specs = parse_and_extract("require Foo::Bar;");
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::Require);
1097 assert_eq!(spec.symbols, ImportSymbols::Default);
1098 assert_eq!(spec.provenance, Provenance::ExactAst);
1099 assert_eq!(spec.confidence, Confidence::High);
1100 assert_eq!(spec.file_id, Some(FileId(1)));
1101 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1102 Ok(())
1103 }
1104
1105 #[test]
1108 fn test_require_then_import_with_qw() -> Result<(), String> {
1109 let code = r#"
1110require Foo::Bar;
1111Foo::Bar->import(qw(alpha beta));
1112"#;
1113 let specs = parse_and_extract(code);
1114 let spec = specs
1115 .iter()
1116 .find(|s| s.module == "Foo::Bar")
1117 .ok_or("expected ImportSpec for Foo::Bar")?;
1118
1119 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1120 if let ImportSymbols::Explicit(names) = &spec.symbols {
1121 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
1122 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
1123 } else {
1124 return Err(format!("expected Explicit, got {:?}", spec.symbols));
1125 }
1126 assert_eq!(spec.provenance, Provenance::LiteralRequireImport);
1128 assert_eq!(spec.confidence, Confidence::High);
1129 Ok(())
1130 }
1131
1132 #[test]
1133 fn test_require_then_import_bare() -> Result<(), String> {
1134 let code = r#"
1135require Some::Module;
1136Some::Module->import();
1137"#;
1138 let specs = parse_and_extract(code);
1139 let spec = specs
1140 .iter()
1141 .find(|s| s.module == "Some::Module")
1142 .ok_or("expected ImportSpec for Some::Module")?;
1143
1144 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1145 assert_eq!(spec.symbols, ImportSymbols::Default);
1146 Ok(())
1147 }
1148
1149 #[test]
1150 fn test_require_then_import_quoted_strings() -> Result<(), String> {
1151 let code = r#"
1152require Foo::Bar;
1153Foo::Bar->import('alpha', 'beta');
1154"#;
1155 let specs = parse_and_extract(code);
1156 let spec = specs
1157 .iter()
1158 .find(|s| s.module == "Foo::Bar")
1159 .ok_or("expected ImportSpec for Foo::Bar")?;
1160
1161 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1162 if let ImportSymbols::Explicit(names) = &spec.symbols {
1163 assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
1164 assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
1165 } else {
1166 return Err(format!("expected Explicit, got {:?}", spec.symbols));
1167 }
1168 assert_eq!(spec.provenance, Provenance::LiteralRequireImport);
1170 assert_eq!(spec.confidence, Confidence::High);
1171 Ok(())
1172 }
1173
1174 #[test]
1175 fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
1176 let code = r#"
1177require Foo::Bar;
1178Foo::Bar->import(@names);
1179"#;
1180 let specs = parse_and_extract(code);
1181 let spec = specs
1182 .iter()
1183 .find(|s| s.module == "Foo::Bar")
1184 .ok_or("expected ImportSpec for Foo::Bar")?;
1185
1186 assert_eq!(spec.kind, ImportKind::RequireThenImport);
1187 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1188 assert_eq!(spec.confidence, Confidence::Low);
1189 Ok(())
1190 }
1191
1192 #[test]
1195 fn test_require_dynamic_variable() -> Result<(), String> {
1196 let specs = parse_and_extract("require $module;");
1197 let spec = specs
1198 .iter()
1199 .find(|s| s.kind == ImportKind::DynamicRequire)
1200 .ok_or("expected DynamicRequire ImportSpec")?;
1201
1202 assert_eq!(spec.module, "");
1203 assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1204 assert_eq!(spec.provenance, Provenance::DynamicBoundary);
1207 assert_eq!(spec.confidence, Confidence::Low);
1208 assert_eq!(spec.file_id, Some(FileId(1)));
1209 assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1210 Ok(())
1211 }
1212
1213 #[test]
1216 fn test_mixed_use_and_require() -> Result<(), String> {
1217 let code = r#"
1218use strict;
1219use warnings;
1220require Foo::Bar;
1221Foo::Bar->import(qw(baz));
1222require $dynamic;
1223"#;
1224 let specs = parse_and_extract(code);
1225
1226 let strict_spec =
1228 specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
1229 assert_eq!(strict_spec.kind, ImportKind::Use);
1230
1231 let warnings_spec =
1233 specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
1234 assert_eq!(warnings_spec.kind, ImportKind::Use);
1235
1236 let foo_spec =
1238 specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
1239 assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
1240 if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
1241 assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
1242 } else {
1243 return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
1244 }
1245
1246 let dyn_spec = specs
1248 .iter()
1249 .find(|s| s.kind == ImportKind::DynamicRequire)
1250 .ok_or("expected DynamicRequire ImportSpec")?;
1251 assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
1252
1253 Ok(())
1254 }
1255
1256 #[test]
1259 fn test_require_string_path() -> Result<(), String> {
1260 let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
1261 let spec = specs
1262 .iter()
1263 .find(|s| s.module == "Foo::Bar")
1264 .ok_or("expected ImportSpec for Foo::Bar")?;
1265
1266 assert_eq!(spec.kind, ImportKind::Require);
1267 assert_eq!(spec.symbols, ImportSymbols::Default);
1268 Ok(())
1269 }
1270
1271 #[test]
1274 fn standalone_class_dynamic_import_produces_dynamic_spec() -> Result<(), String> {
1275 let specs = parse_and_extract(r#"Foo->import(@names);"#);
1279 let spec = specs
1280 .iter()
1281 .find(|s| s.module == "Foo" && matches!(s.symbols, ImportSymbols::Dynamic))
1282 .ok_or("expected Dynamic ImportSpec for Foo")?;
1283
1284 assert_eq!(spec.provenance, Provenance::DynamicBoundary);
1285 assert_eq!(spec.confidence, Confidence::Low);
1286 assert_eq!(
1287 spec.kind,
1288 ImportKind::ManualImport,
1289 "Class->import(@names) must use ManualImport, not Use"
1290 );
1291 Ok(())
1292 }
1293
1294 #[test]
1295 fn standalone_class_explicit_import_produces_no_dynamic_spec() -> Result<(), String> {
1296 let specs = parse_and_extract(r#"Foo->import('bar');"#);
1299 let dynamic_specs: Vec<_> =
1300 specs.iter().filter(|s| matches!(s.symbols, ImportSymbols::Dynamic)).collect();
1301
1302 assert!(dynamic_specs.is_empty(), "explicit import args must not produce a Dynamic spec");
1303 Ok(())
1304 }
1305
1306 #[test]
1307 fn variable_class_import_does_not_produce_standalone_spec() -> Result<(), String> {
1308 let specs = parse_and_extract(r#"$var->import(@names);"#);
1311 let standalone_dynamic: Vec<_> = specs
1314 .iter()
1315 .filter(|s| matches!(s.symbols, ImportSymbols::Dynamic) && s.module.is_empty())
1316 .collect();
1317
1318 assert!(
1321 standalone_dynamic.is_empty(),
1322 "variable-class import without require must not produce standalone Dynamic spec"
1323 );
1324 Ok(())
1325 }
1326}