use perl_module::{RequireImportEntry, extract_require_import_symbols};
#[test]
fn qw_list_produces_entries() -> Result<(), String> {
let source = "require Foo::Bar;\nFoo::Bar->import(qw(alpha beta));\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 2 {
return Err(format!("expected 2 entries, got {}: {entries:?}", entries.len()));
}
for e in &entries {
if e.module != "Foo::Bar" {
return Err(format!("unexpected module {:?}", e.module));
}
}
let names: Vec<&str> = entries.iter().map(|e| e.symbol.as_str()).collect();
if !names.contains(&"alpha") {
return Err(format!("missing 'alpha' in {names:?}"));
}
if !names.contains(&"beta") {
return Err(format!("missing 'beta' in {names:?}"));
}
Ok(())
}
#[test]
fn single_quoted_args_produce_entries() -> Result<(), String> {
let source = "require Foo;\nFoo->import('foo', 'bar');\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 2 {
return Err(format!("expected 2 entries, got {}: {entries:?}", entries.len()));
}
let names: Vec<&str> = entries.iter().map(|e| e.symbol.as_str()).collect();
if !names.contains(&"foo") {
return Err(format!("missing 'foo' in {names:?}"));
}
if !names.contains(&"bar") {
return Err(format!("missing 'bar' in {names:?}"));
}
Ok(())
}
#[test]
fn double_quoted_args_produce_entries() -> Result<(), String> {
let source = "require Foo;\nFoo->import(\"foo\", \"bar\");\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 2 {
return Err(format!("expected 2 entries, got {}: {entries:?}", entries.len()));
}
let names: Vec<&str> = entries.iter().map(|e| e.symbol.as_str()).collect();
if !names.contains(&"foo") {
return Err(format!("missing 'foo' in {names:?}"));
}
if !names.contains(&"bar") {
return Err(format!("missing 'bar' in {names:?}"));
}
Ok(())
}
#[test]
fn same_line_require_import_pair_is_extracted() -> Result<(), String> {
let source = "require Foo; Foo->import(qw(alpha beta));\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 2 {
return Err(format!("expected 2 entries, got {}: {entries:?}", entries.len()));
}
let names: Vec<&str> = entries.iter().map(|e| e.symbol.as_str()).collect();
if !names.contains(&"alpha") || !names.contains(&"beta") {
return Err(format!("missing same-line imports in {names:?}"));
}
if entries.iter().any(|e| e.import_byte_offset != 13) {
return Err(format!("expected import_byte_offset=13, got {entries:?}"));
}
Ok(())
}
#[test]
fn indented_require_and_import_offsets_are_precise() -> Result<(), String> {
let source = " require Foo;\n Foo->import('bar');\n";
let entries = extract_require_import_symbols(source);
let entry = entries.first().ok_or("expected one import entry")?;
if entry.require_byte_offset != 2 {
return Err(format!(
"expected indented require_byte_offset=2, got {}",
entry.require_byte_offset
));
}
if entry.import_byte_offset != 19 {
return Err(format!(
"expected indented import_byte_offset=19, got {}",
entry.import_byte_offset
));
}
Ok(())
}
#[test]
fn malformed_bareword_module_names_are_rejected() -> Result<(), String> {
let source = "require Foo:::Bar;\nFoo:::Bar->import('baz');\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for malformed module name, got {entries:?}"));
}
Ok(())
}
#[test]
fn whitespace_around_import_method_call_is_tolerated() -> Result<(), String> {
let source = "require Foo::Bar;\nFoo::Bar -> import ( 'alpha', \"beta\" );\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 2 {
return Err(format!("expected 2 entries, got {}: {entries:?}", entries.len()));
}
let names: Vec<&str> = entries.iter().map(|e| e.symbol.as_str()).collect();
if !names.contains(&"alpha") || !names.contains(&"beta") {
return Err(format!("missing symbols in {names:?}"));
}
Ok(())
}
#[test]
fn qw_list_with_non_paren_delimiter_produces_entries() -> Result<(), String> {
let cases = [
("Foo->import(qw[alpha beta]);", ["alpha", "beta"]),
("Foo->import(qw{gamma delta});", ["gamma", "delta"]),
("Foo->import(qw<epsilon zeta>);", ["epsilon", "zeta"]),
("Foo->import(qw/eta theta/);", ["eta", "theta"]),
("Foo->import(qw!iota kappa!);", ["iota", "kappa"]),
];
for (import_line, expected_names) in cases {
let source = format!("require Foo;\n{import_line}\n");
let entries = extract_require_import_symbols(&source);
let names: Vec<&str> = entries.iter().map(|entry| entry.symbol.as_str()).collect();
if names != expected_names {
return Err(format!("expected {expected_names:?} for {import_line:?}, got {names:?}"));
}
}
Ok(())
}
#[test]
fn malformed_qw_list_is_rejected() -> Result<(), String> {
let source = "require Foo;\nFoo->import(qw[alpha beta));\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for malformed qw list, got {entries:?}"));
}
Ok(())
}
#[test]
fn nested_module_name_is_preserved() -> Result<(), String> {
let source = "require Module::Nested;\nModule::Nested->import('foo');\n";
let entries = extract_require_import_symbols(source);
if entries.len() != 1 {
return Err(format!("expected 1 entry, got {}: {entries:?}", entries.len()));
}
let e = &entries[0];
if e.module != "Module::Nested" {
return Err(format!("wrong module {:?}", e.module));
}
if e.symbol != "foo" {
return Err(format!("wrong symbol {:?}", e.symbol));
}
Ok(())
}
#[test]
fn dynamic_module_name_is_rejected() -> Result<(), String> {
let source = "require $module;\n$module->import('foo');\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for dynamic require, got {entries:?}"));
}
Ok(())
}
#[test]
fn dynamic_array_arg_is_rejected() -> Result<(), String> {
let source = "require Foo;\nFoo->import(@list);\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for dynamic arg list, got {entries:?}"));
}
Ok(())
}
#[test]
fn dynamic_scalar_arg_is_rejected() -> Result<(), String> {
let source = "require Foo;\nFoo->import($sym);\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for dynamic scalar arg, got {entries:?}"));
}
Ok(())
}
#[test]
fn variable_receiver_is_rejected() -> Result<(), String> {
let source = "require Foo;\n$class->import('x');\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for variable-receiver import, got {entries:?}"));
}
Ok(())
}
#[test]
fn quoted_file_path_require_is_rejected() -> Result<(), String> {
let source = "require \"Foo/Bar.pm\";\nFoo::Bar->import('baz');\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for quoted file-path require, got {entries:?}"));
}
Ok(())
}
#[test]
fn byte_offsets_are_populated() -> Result<(), String> {
let source = "require Foo;\nFoo->import(qw(bar));\n";
let entries = extract_require_import_symbols(source);
let e = entries.first().ok_or("expected at least one entry")?;
if e.require_byte_offset != 0 {
return Err(format!("expected require_byte_offset=0, got {}", e.require_byte_offset));
}
if e.import_byte_offset != 13 {
return Err(format!("expected import_byte_offset=13, got {}", e.import_byte_offset));
}
Ok(())
}
#[test]
fn multiple_require_import_pairs_all_extracted() -> Result<(), String> {
let source = "\
require A;
A->import('x');
require B;
B->import(qw(y z));
";
let entries = extract_require_import_symbols(source);
let a_entries: Vec<&RequireImportEntry> = entries.iter().filter(|e| e.module == "A").collect();
let b_entries: Vec<&RequireImportEntry> = entries.iter().filter(|e| e.module == "B").collect();
if a_entries.len() != 1 {
return Err(format!("expected 1 entry for A, got {}: {a_entries:?}", a_entries.len()));
}
if b_entries.len() != 2 {
return Err(format!("expected 2 entries for B, got {}: {b_entries:?}", b_entries.len()));
}
if a_entries[0].symbol != "x" {
return Err(format!("wrong symbol for A: {:?}", a_entries[0].symbol));
}
let b_names: Vec<&str> = b_entries.iter().map(|e| e.symbol.as_str()).collect();
if !b_names.contains(&"y") || !b_names.contains(&"z") {
return Err(format!("wrong symbols for B: {b_names:?}"));
}
Ok(())
}
#[test]
fn empty_source_returns_empty() -> Result<(), String> {
let entries = extract_require_import_symbols("");
if !entries.is_empty() {
return Err(format!("expected empty for empty source, got {entries:?}"));
}
Ok(())
}
#[test]
fn require_without_import_produces_no_entries() -> Result<(), String> {
let source = "require Foo;\n";
let entries = extract_require_import_symbols(source);
if !entries.is_empty() {
return Err(format!("expected no entries for require without import, got {entries:?}"));
}
Ok(())
}