use crate::ast::{Node, NodeKind};
use perl_semantic_facts::{
AnchorId, Confidence, FileId, ImportKind, ImportSpec, ImportSymbols, Provenance,
};
pub struct ImportExtractor;
impl ImportExtractor {
pub fn extract(ast: &Node, file_id: FileId) -> Vec<ImportSpec> {
let mut specs = Vec::new();
Self::walk(ast, file_id, &mut specs);
specs
}
fn walk(node: &Node, file_id: FileId, out: &mut Vec<ImportSpec>) {
if let NodeKind::Use { module, args, .. } = &node.kind {
if let Some(spec) = Self::classify_use(module, args, file_id, node) {
out.push(spec);
}
}
match &node.kind {
NodeKind::Program { statements } | NodeKind::Block { statements } => {
Self::walk_statements(statements, file_id, out);
}
NodeKind::Package { block: Some(block), .. } => {
if let NodeKind::Block { statements } = &block.kind {
Self::walk_statements(statements, file_id, out);
}
}
_ => {}
}
for child in node.children() {
Self::walk(child, file_id, out);
}
}
fn walk_statements(statements: &[Node], file_id: FileId, out: &mut Vec<ImportSpec>) {
let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
for (i, stmt) in statements.iter().enumerate() {
if consumed.contains(&i) {
continue;
}
let expr = Self::unwrap_expression_statement(stmt);
let (require_node, require_args) = match &expr.kind {
NodeKind::FunctionCall { name, args } if name == "require" => (stmt, args),
_ => continue,
};
if Self::is_dynamic_require(require_args) {
out.push(Self::make_dynamic_require(file_id, require_node));
consumed.insert(i);
continue;
}
let module_name = match Self::extract_require_module_name(require_args) {
Some(name) => name,
None => continue,
};
let import_spec = if let Some(next_stmt) = statements.get(i + 1) {
let next_expr = Self::unwrap_expression_statement(next_stmt);
Self::try_match_import_call(next_expr, &module_name)
} else {
None
};
if let Some((symbols, import_node)) = import_spec {
let anchor_id = Self::anchor_from_node(require_node);
let confidence = Self::confidence_for_symbols(&symbols);
out.push(ImportSpec {
module: module_name,
kind: ImportKind::RequireThenImport,
symbols,
provenance: Provenance::ExactAst,
confidence,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
});
consumed.insert(i);
consumed.insert(i + 1);
let _ = import_node;
} else {
let anchor_id = Self::anchor_from_node(require_node);
out.push(ImportSpec {
module: module_name,
kind: ImportKind::Require,
symbols: ImportSymbols::Default,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
});
consumed.insert(i);
}
}
}
fn unwrap_expression_statement(node: &Node) -> &Node {
match &node.kind {
NodeKind::ExpressionStatement { expression } => expression,
_ => node,
}
}
fn is_dynamic_require(args: &[Node]) -> bool {
match args.first() {
Some(arg) => matches!(&arg.kind, NodeKind::Variable { .. }),
None => false,
}
}
fn extract_require_module_name(args: &[Node]) -> Option<String> {
let arg = args.first()?;
match &arg.kind {
NodeKind::Identifier { name } => Some(name.clone()),
NodeKind::String { value, .. } => {
let cleaned = value.trim_matches('\'').trim_matches('"').trim();
let module = cleaned.trim_end_matches(".pm").replace('/', "::");
Some(module)
}
_ => None,
}
}
fn make_dynamic_require(file_id: FileId, node: &Node) -> ImportSpec {
let anchor_id = Self::anchor_from_node(node);
ImportSpec {
module: String::new(),
kind: ImportKind::DynamicRequire,
symbols: ImportSymbols::Dynamic,
provenance: Provenance::DynamicBoundary,
confidence: Confidence::Low,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
}
}
fn try_match_import_call<'a>(
node: &'a Node,
expected_module: &str,
) -> Option<(ImportSymbols, &'a Node)> {
let (object, method, args) = match &node.kind {
NodeKind::MethodCall { object, method, args } => (object, method, args),
_ => return None,
};
if method != "import" {
return None;
}
let obj_name = match &object.kind {
NodeKind::Identifier { name } => name.as_str(),
_ => return None,
};
if obj_name != expected_module {
return None;
}
let symbols = Self::extract_import_call_symbols(args);
Some((symbols, node))
}
fn extract_import_call_symbols(args: &[Node]) -> ImportSymbols {
if args.is_empty() {
return ImportSymbols::Default;
}
let mut names: Vec<String> = Vec::new();
let mut tags: Vec<String> = Vec::new();
let mut has_dynamic_arg = false;
for arg in args {
has_dynamic_arg |= Self::collect_import_arg_symbols(arg, &mut names, &mut tags);
}
if has_dynamic_arg {
return ImportSymbols::Dynamic;
}
if names.is_empty() && tags.is_empty() {
return ImportSymbols::Default;
}
if !tags.is_empty() && names.is_empty() {
return ImportSymbols::Tags(tags);
}
if !tags.is_empty() && !names.is_empty() {
return ImportSymbols::Mixed { tags, names };
}
ImportSymbols::Explicit(names)
}
fn collect_import_arg_symbols(
arg: &Node,
names: &mut Vec<String>,
tags: &mut Vec<String>,
) -> bool {
match &arg.kind {
NodeKind::String { value, .. } => {
let bare = value.trim_matches('\'').trim_matches('"');
if let Some(tag) = bare.strip_prefix(':') {
tags.push(tag.to_string());
} else if !bare.is_empty() {
names.push(bare.to_string());
}
false
}
NodeKind::Identifier { name } => {
if let Some(inner) = Self::parse_qw_content(name) {
for word in inner.split_whitespace() {
if let Some(tag) = word.strip_prefix(':') {
tags.push(tag.to_string());
} else {
names.push(word.to_string());
}
}
} else if let Some(tag) = name.strip_prefix(':') {
tags.push(tag.to_string());
} else if !name.is_empty() {
names.push(name.clone());
}
false
}
NodeKind::Variable { .. } => {
true
}
NodeKind::ArrayLiteral { elements } => {
let mut has_dynamic_arg = false;
for el in elements {
has_dynamic_arg |= Self::collect_import_arg_symbols(el, names, tags);
}
has_dynamic_arg
}
_ => true,
}
}
fn confidence_for_symbols(symbols: &ImportSymbols) -> Confidence {
if matches!(symbols, ImportSymbols::Dynamic) { Confidence::Low } else { Confidence::High }
}
fn classify_use(
module: &str,
args: &[String],
file_id: FileId,
node: &Node,
) -> Option<ImportSpec> {
if Self::is_version_pragma(module) {
return None;
}
let anchor_id = Self::anchor_from_node(node);
if module == "constant" {
return Some(Self::classify_use_constant(args, file_id, anchor_id));
}
let (kind, symbols) = Self::classify_args(args, module, node);
Some(ImportSpec {
module: module.to_string(),
kind,
symbols,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
})
}
fn classify_args(args: &[String], module: &str, node: &Node) -> (ImportKind, ImportSymbols) {
if args.is_empty() {
let bare_len = "use ".len() + module.len() + 1; let span_len = node.location.end.saturating_sub(node.location.start);
if span_len > bare_len {
return (ImportKind::UseEmpty, ImportSymbols::None);
}
return (ImportKind::Use, ImportSymbols::Default);
}
let mut explicit_names: Vec<String> = Vec::new();
let mut tags: Vec<String> = Vec::new();
for arg in args {
let trimmed = arg.trim();
if let Some(inner) = Self::parse_qw_content(trimmed) {
let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
for word in words {
if let Some(tag) = word.strip_prefix(':') {
tags.push(tag.to_string());
} else {
explicit_names.push(word);
}
}
continue;
}
let unquoted = Self::unquote(trimmed);
if let Some(tag) = unquoted.strip_prefix(':') {
tags.push(tag.to_string());
continue;
}
if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
continue;
}
if Self::looks_like_symbol_name(trimmed) {
explicit_names.push(Self::unquote(trimmed).to_string());
}
}
if explicit_names.is_empty() && tags.is_empty() && !args.is_empty() {
let has_any_symbol = args.iter().any(|a| {
let t = a.trim();
Self::looks_like_symbol_name(t) || Self::parse_qw_content(t).is_some()
});
if !has_any_symbol {
return (ImportKind::UseEmpty, ImportSymbols::None);
}
}
if !tags.is_empty() && explicit_names.is_empty() {
return (ImportKind::UseTag, ImportSymbols::Tags(tags));
}
if !tags.is_empty() && !explicit_names.is_empty() {
return (
ImportKind::UseExplicitList,
ImportSymbols::Mixed { tags, names: explicit_names },
);
}
if !explicit_names.is_empty() {
return (ImportKind::UseExplicitList, ImportSymbols::Explicit(explicit_names));
}
(ImportKind::Use, ImportSymbols::Default)
}
fn classify_use_constant(args: &[String], file_id: FileId, anchor_id: AnchorId) -> ImportSpec {
let mut constant_names: Vec<String> = Vec::new();
if args.is_empty() {
return ImportSpec {
module: "constant".to_string(),
kind: ImportKind::UseConstant,
symbols: ImportSymbols::None,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
};
}
if args.first().map(|a| a.as_str()) == Some("{") {
let mut i = 1; while i < args.len() {
let token = args[i].trim();
if token == "}" || token == "=>" || token == "," {
i += 1;
continue;
}
if i + 1 < args.len() && args[i + 1].trim() == "=>" {
constant_names.push(token.to_string());
i += 3;
} else {
i += 1;
}
}
}
else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
constant_names.extend(words);
}
else if let Some(name) = args.first() {
let trimmed = name.trim();
if Self::looks_like_constant_name(trimmed) {
constant_names.push(trimmed.to_string());
}
}
let mut seen = std::collections::HashSet::new();
constant_names.retain(|n| seen.insert(n.clone()));
let symbols = if constant_names.is_empty() {
ImportSymbols::None
} else {
ImportSymbols::Explicit(constant_names)
};
ImportSpec {
module: "constant".to_string(),
kind: ImportKind::UseConstant,
symbols,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
file_id: Some(file_id),
anchor_id: Some(anchor_id),
scope_id: None,
}
}
fn anchor_from_node(node: &Node) -> AnchorId {
AnchorId(node.location.start as u64)
}
fn is_version_pragma(module: &str) -> bool {
if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return true;
}
if module.starts_with('v')
&& module.len() > 1
&& module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
{
return true;
}
false
}
fn parse_qw_content(s: &str) -> Option<&str> {
let rest = s.strip_prefix("qw")?;
let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
Some(inner)
}
fn unquote(s: &str) -> &str {
if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
if s.len() >= 2 {
return &s[1..s.len() - 1];
}
}
s
}
fn looks_like_symbol_name(s: &str) -> bool {
let s = Self::unquote(s);
if s.is_empty() {
return false;
}
if s.starts_with(':') {
return true;
}
if s.starts_with('$')
|| s.starts_with('@')
|| s.starts_with('%')
|| s.starts_with('&')
|| s.starts_with('*')
{
return true;
}
s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
}
fn looks_like_constant_name(s: &str) -> bool {
if s.is_empty() {
return false;
}
s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Parser;
fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
let mut parser = Parser::new(code);
let ast = match parser.parse() {
Ok(ast) => ast,
Err(_) => return Vec::new(),
};
ImportExtractor::extract(&ast, FileId(1))
}
#[test]
fn test_use_explicit_list_qw() -> Result<(), String> {
let specs = parse_and_extract("use List::Util qw(first reduce any);");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "List::Util");
assert_eq!(spec.kind, ImportKind::UseExplicitList);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
assert_eq!(spec.file_id, Some(FileId(1)));
assert!(spec.anchor_id.is_some());
Ok(())
}
#[test]
fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
let specs = parse_and_extract("use Exporter 'import';");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "Exporter");
assert_eq!(spec.kind, ImportKind::UseExplicitList);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_use_empty_parens() -> Result<(), String> {
let specs = parse_and_extract("use POSIX ();");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "POSIX");
assert_eq!(spec.kind, ImportKind::UseEmpty);
assert_eq!(spec.symbols, ImportSymbols::None);
Ok(())
}
#[test]
fn test_use_tag_single() -> Result<(), String> {
let specs = parse_and_extract("use POSIX ':sys_wait_h';");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "POSIX");
assert_eq!(spec.kind, ImportKind::UseTag);
if let ImportSymbols::Tags(tags) = &spec.symbols {
assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
} else {
return Err(format!("expected Tags, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_use_tag_in_qw() -> Result<(), String> {
let specs = parse_and_extract("use Fcntl qw(:flock);");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "Fcntl");
assert_eq!(spec.kind, ImportKind::UseTag);
if let ImportSymbols::Tags(tags) = &spec.symbols {
assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
} else {
return Err(format!("expected Tags, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_use_bare() -> Result<(), String> {
let specs = parse_and_extract("use strict;");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "strict");
assert_eq!(spec.kind, ImportKind::Use);
assert_eq!(spec.symbols, ImportSymbols::Default);
Ok(())
}
#[test]
fn test_use_bare_qualified() -> Result<(), String> {
let specs = parse_and_extract("use Data::Dumper;");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "Data::Dumper");
assert_eq!(spec.kind, ImportKind::Use);
assert_eq!(spec.symbols, ImportSymbols::Default);
Ok(())
}
#[test]
fn test_use_constant_scalar() -> Result<(), String> {
let specs = parse_and_extract("use constant PI => 3.14;");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "constant");
assert_eq!(spec.kind, ImportKind::UseConstant);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_use_constant_hash_ref() -> Result<(), String> {
let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "constant");
assert_eq!(spec.kind, ImportKind::UseConstant);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_use_constant_empty() -> Result<(), String> {
let specs = parse_and_extract("use constant;");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "constant");
assert_eq!(spec.kind, ImportKind::UseConstant);
assert_eq!(spec.symbols, ImportSymbols::None);
Ok(())
}
#[test]
fn test_version_pragma_skipped() -> Result<(), String> {
let specs = parse_and_extract("use 5.036;");
assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
Ok(())
}
#[test]
fn test_vstring_pragma_skipped() -> Result<(), String> {
let specs = parse_and_extract("use v5.38;");
assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
Ok(())
}
#[test]
fn test_multiple_use_statements() -> Result<(), String> {
let code = r#"
use strict;
use warnings;
use List::Util qw(first any);
use POSIX ();
use constant MAX => 100;
"#;
let specs = parse_and_extract(code);
assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
assert_eq!(specs[0].module, "strict");
assert_eq!(specs[0].kind, ImportKind::Use);
assert_eq!(specs[1].module, "warnings");
assert_eq!(specs[1].kind, ImportKind::Use);
assert_eq!(specs[2].module, "List::Util");
assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
assert_eq!(specs[3].module, "POSIX");
assert_eq!(specs[3].kind, ImportKind::UseEmpty);
assert_eq!(specs[4].module, "constant");
assert_eq!(specs[4].kind, ImportKind::UseConstant);
Ok(())
}
#[test]
fn test_anchor_and_file_id_populated() -> Result<(), String> {
let specs = parse_and_extract("use Foo::Bar qw(baz);");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.file_id, Some(FileId(1)));
assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
assert_eq!(spec.provenance, Provenance::ExactAst);
assert_eq!(spec.confidence, Confidence::High);
Ok(())
}
#[test]
fn test_use_inside_package_block() -> Result<(), String> {
let code = r#"
package MyModule;
use Exporter 'import';
our @EXPORT = qw(foo);
1;
"#;
let specs = parse_and_extract(code);
let exporter_spec =
specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
assert!(names.contains(&"import".to_string()));
} else {
return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
}
Ok(())
}
#[test]
fn test_use_mixed_tags_and_names() -> Result<(), String> {
let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
let spec = specs.first().ok_or("expected at least one ImportSpec")?;
assert_eq!(spec.module, "Fcntl");
assert_eq!(spec.kind, ImportKind::UseExplicitList);
if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
} else {
return Err(format!("expected Mixed, got {:?}", spec.symbols));
}
Ok(())
}
#[test]
fn test_require_bare_module() -> Result<(), String> {
let specs = parse_and_extract("require Foo::Bar;");
let spec = specs
.iter()
.find(|s| s.module == "Foo::Bar")
.ok_or("expected ImportSpec for Foo::Bar")?;
assert_eq!(spec.kind, ImportKind::Require);
assert_eq!(spec.symbols, ImportSymbols::Default);
assert_eq!(spec.provenance, Provenance::ExactAst);
assert_eq!(spec.confidence, Confidence::High);
assert_eq!(spec.file_id, Some(FileId(1)));
assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
Ok(())
}
#[test]
fn test_require_then_import_with_qw() -> Result<(), String> {
let code = r#"
require Foo::Bar;
Foo::Bar->import(qw(alpha beta));
"#;
let specs = parse_and_extract(code);
let spec = specs
.iter()
.find(|s| s.module == "Foo::Bar")
.ok_or("expected ImportSpec for Foo::Bar")?;
assert_eq!(spec.kind, ImportKind::RequireThenImport);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
assert_eq!(spec.provenance, Provenance::ExactAst);
assert_eq!(spec.confidence, Confidence::High);
Ok(())
}
#[test]
fn test_require_then_import_bare() -> Result<(), String> {
let code = r#"
require Some::Module;
Some::Module->import();
"#;
let specs = parse_and_extract(code);
let spec = specs
.iter()
.find(|s| s.module == "Some::Module")
.ok_or("expected ImportSpec for Some::Module")?;
assert_eq!(spec.kind, ImportKind::RequireThenImport);
assert_eq!(spec.symbols, ImportSymbols::Default);
Ok(())
}
#[test]
fn test_require_then_import_quoted_strings() -> Result<(), String> {
let code = r#"
require Foo::Bar;
Foo::Bar->import('alpha', 'beta');
"#;
let specs = parse_and_extract(code);
let spec = specs
.iter()
.find(|s| s.module == "Foo::Bar")
.ok_or("expected ImportSpec for Foo::Bar")?;
assert_eq!(spec.kind, ImportKind::RequireThenImport);
if let ImportSymbols::Explicit(names) = &spec.symbols {
assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", spec.symbols));
}
assert_eq!(spec.confidence, Confidence::High);
Ok(())
}
#[test]
fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
let code = r#"
require Foo::Bar;
Foo::Bar->import(@names);
"#;
let specs = parse_and_extract(code);
let spec = specs
.iter()
.find(|s| s.module == "Foo::Bar")
.ok_or("expected ImportSpec for Foo::Bar")?;
assert_eq!(spec.kind, ImportKind::RequireThenImport);
assert_eq!(spec.symbols, ImportSymbols::Dynamic);
assert_eq!(spec.confidence, Confidence::Low);
Ok(())
}
#[test]
fn test_require_dynamic_variable() -> Result<(), String> {
let specs = parse_and_extract("require $module;");
let spec = specs
.iter()
.find(|s| s.kind == ImportKind::DynamicRequire)
.ok_or("expected DynamicRequire ImportSpec")?;
assert_eq!(spec.module, "");
assert_eq!(spec.symbols, ImportSymbols::Dynamic);
assert_eq!(spec.provenance, Provenance::DynamicBoundary);
assert_eq!(spec.confidence, Confidence::Low);
assert_eq!(spec.file_id, Some(FileId(1)));
assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
Ok(())
}
#[test]
fn test_mixed_use_and_require() -> Result<(), String> {
let code = r#"
use strict;
use warnings;
require Foo::Bar;
Foo::Bar->import(qw(baz));
require $dynamic;
"#;
let specs = parse_and_extract(code);
let strict_spec =
specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
assert_eq!(strict_spec.kind, ImportKind::Use);
let warnings_spec =
specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
assert_eq!(warnings_spec.kind, ImportKind::Use);
let foo_spec =
specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
} else {
return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
}
let dyn_spec = specs
.iter()
.find(|s| s.kind == ImportKind::DynamicRequire)
.ok_or("expected DynamicRequire ImportSpec")?;
assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
Ok(())
}
#[test]
fn test_require_string_path() -> Result<(), String> {
let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
let spec = specs
.iter()
.find(|s| s.module == "Foo::Bar")
.ok_or("expected ImportSpec for Foo::Bar")?;
assert_eq!(spec.kind, ImportKind::Require);
assert_eq!(spec.symbols, ImportSymbols::Default);
Ok(())
}
}