#[derive(Debug, Clone, Copy)]
pub struct EdgeCase {
pub id: &'static str,
pub description: &'static str,
pub tags: &'static [&'static str],
pub source: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct ComplexDataStructureCase {
pub id: &'static str,
pub description: &'static str,
pub source: &'static str,
}
static EDGE_CASES: [EdgeCase; 103] = [
EdgeCase {
id: "heredoc.basic",
description: "Basic quoted heredoc with multiple lines.",
tags: &["heredoc", "edge-case"],
source: r#"my $text = <<'EOF';
line one
line two
EOF
"#,
},
EdgeCase {
id: "heredoc.indented",
description: "Indented heredoc using <<~ syntax.",
tags: &["heredoc", "edge-case"],
source: r#"my $text = <<~EOF;
indented line
EOF
"#,
},
EdgeCase {
id: "heredoc.multiple.inline",
description: "Multiple heredocs declared on one line.",
tags: &["heredoc", "edge-case", "parser-sensitive"],
source: r#"print <<'A', <<'B';
alpha
A
beta
B
"#,
},
EdgeCase {
id: "heredoc.terminator.content",
description: "Heredoc content containing the terminator text inline.",
tags: &["heredoc", "edge-case", "parser-sensitive"],
source: r#"my $text = <<'END';
This line mentions END but is not the terminator.
ENDINGS are tricky too.
END
"#,
},
EdgeCase {
id: "quote.like",
description: "Quote-like operator with interpolation.",
tags: &["quote-like", "interpolation"],
source: r#"my $name = "Ada";
my $text = qq{Hello $name};
"#,
},
EdgeCase {
id: "quote.delimiters.complex",
description: "Quote-like operators with mixed delimiters.",
tags: &["quote-like", "delimiter", "edge-case"],
source: r#"my $raw = q!literal!;
my $interp = qq{value=$raw};
my $cmd = qx|echo ok|;
my $re = qr#foo.+bar#i;
"#,
},
EdgeCase {
id: "regex.code",
description: "Regex with embedded code block.",
tags: &["regex", "regex-code", "edge-case"],
source: r#"my $count = 0;
"x" =~ /(?{ $count++ })x/;
"#,
},
EdgeCase {
id: "regex.named.capture",
description: "Regex with named capture and hash access.",
tags: &["regex", "edge-case"],
source: r#"my $text = "abc";
if ($text =~ /(?<word>abc)/) {
print $+{word};
}
"#,
},
EdgeCase {
id: "regex.unicode.property",
description: "Regex with Unicode property class.",
tags: &["regex", "unicode", "edge-case"],
source: r#"my $text = "Cafe";
if ($text =~ /\p{Latin}+/) {
print "ok";
}
"#,
},
EdgeCase {
id: "substitution.balanced",
description: "Substitution with balanced delimiters and modifiers.",
tags: &["substitution", "regex", "edge-case"],
source: r#"my $text = "foo bar";
$text =~ s{foo}{bar}g;
"#,
},
EdgeCase {
id: "substitution.nondestructive",
description: "Non-destructive substitution with the r modifier.",
tags: &["substitution", "regex", "edge-case"],
source: r#"my $text = "foo bar";
my $updated = $text =~ s/foo/baz/r;
"#,
},
EdgeCase {
id: "transliteration.basic",
description: "Transliteration with character ranges.",
tags: &["transliteration", "tr", "edge-case"],
source: r#"my $text = "abc";
$text =~ tr/a-z/A-Z/;
"#,
},
EdgeCase {
id: "transliteration.alias",
description: "Transliteration using the y/// alias operator.",
tags: &["transliteration", "tr", "edge-case"],
source: r#"my $text = "abc";
$text =~ y/a-z/A-Z/;
"#,
},
EdgeCase {
id: "map.grep",
description: "Map/grep with block syntax.",
tags: &["map", "grep", "list-context"],
source: r#"my @nums = (1, 2, 3);
my @doubled = map { $_ * 2 } @nums;
my @even = grep { $_ % 2 == 0 } @nums;
"#,
},
EdgeCase {
id: "map.empty.block",
description: "Map with an empty block.",
tags: &["map", "list-context", "edge-case"],
source: r#"my @nums = (1, 2, 3);
my @mapped = map { } @nums;
"#,
},
EdgeCase {
id: "grep.empty.block",
description: "Grep with an empty block.",
tags: &["grep", "list-context", "edge-case"],
source: r#"my @nums = (1, 2, 3);
my @kept = grep { } @nums;
"#,
},
EdgeCase {
id: "format.statement",
description: "Format statement with picture lines.",
tags: &["format", "legacy", "edge-case"],
source: r#"my ($name, $age) = ("Ada", 37);
format STDOUT =
@<<<<<< @>>>>>
$name, $age
.
write;
"#,
},
EdgeCase {
id: "format.formline",
description: "Formline builtin with accumulator and picture string.",
tags: &["format", "builtin", "edge-case"],
source: r#"my $picture = "@<<";
formline $picture, "hi";
my $formatted = $^A;
$^A = "";
"#,
},
EdgeCase {
id: "glob.angle",
description: "Glob expression using angle brackets.",
tags: &["glob", "file", "edge-case"],
source: r#"my @files = <*.pl>;
my @more = glob "*.pm";
"#,
},
EdgeCase {
id: "tie.hash",
description: "Tie and untie a hash.",
tags: &["tie", "hash", "edge-case"],
source: r#"tie my %cache, "Tie::StdHash";
$cache{a} = 1;
untie %cache;
"#,
},
EdgeCase {
id: "redo.loop",
description: "Redo inside a loop.",
tags: &["redo", "loop", "edge-case"],
source: r#"my $count = 0;
while ($count < 3) {
$count++;
redo if $count == 2;
}
"#,
},
EdgeCase {
id: "continue.block",
description: "Continue block after a for loop.",
tags: &["continue", "loop", "edge-case"],
source: r#"for my $i (1..3) {
next if $i == 2;
} continue {
my $j = $i * 2;
}
"#,
},
EdgeCase {
id: "defined.or",
description: "Defined-or operator with undef fallback.",
tags: &["defined-or", "operator", "edge-case"],
source: r#"my $value = undef // 42;
"#,
},
EdgeCase {
id: "given.when",
description: "Given/when flow with default branch.",
tags: &["given", "when", "flow", "edge-case"],
source: r#"use v5.10;
my $value = 2;
given ($value) {
when (1) { print "one"; }
when (2) { print "two"; }
default { print "other"; }
}
"#,
},
EdgeCase {
id: "eval.block",
description: "Eval block with error handling.",
tags: &["eval", "error", "edge-case"],
source: r#"eval { die "boom" };
warn $@ if $@;
"#,
},
EdgeCase {
id: "do.block",
description: "Do block returning a computed value.",
tags: &["do", "block", "edge-case"],
source: r#"my $result = do {
my $x = 1;
$x + 1;
};
"#,
},
EdgeCase {
id: "nesting.deep.blocks",
description: "Deeply nested blocks and conditionals.",
tags: &["block", "flow", "edge-case", "parser-sensitive"],
source: r#"my $value = 0;
if ($value) {
if ($value > 1) {
if ($value > 2) {
if ($value > 3) {
if ($value > 4) {
$value++;
}
}
}
}
}
"#,
},
EdgeCase {
id: "package.qualified",
description: "Package-qualified subroutine call.",
tags: &["package", "subroutine", "edge-case"],
source: r#"My::Pkg::helper();
"#,
},
EdgeCase {
id: "signature.defaults",
description: "Subroutine signatures with defaults and slurpy params.",
tags: &["signature", "subroutine", "edge-case"],
source: r#"sub add($x, $y = 0, @rest) {
return $x + $y + @rest;
}
"#,
},
EdgeCase {
id: "signature.attribute",
description: "Subroutine signature paired with an lvalue attribute.",
tags: &["signature", "subroutine", "edge-case"],
source: r#"use feature 'signatures';
no warnings 'experimental::signatures';
my $value = 1;
sub getter ($self) :lvalue { return $value; }
"#,
},
EdgeCase {
id: "package.block",
description: "Package block with nested subroutine.",
tags: &["package", "subroutine", "edge-case"],
source: r#"package Foo::Bar {
sub helper { return 1; }
}
"#,
},
EdgeCase {
id: "method.chain",
description: "Chained method calls with arrows.",
tags: &["method", "arrow", "edge-case"],
source: r#"my $value = $obj->foo->bar(1, 2);
"#,
},
EdgeCase {
id: "try.catch.finally",
description: "Try/catch/finally control flow.",
tags: &["try", "catch", "finally", "edge-case"],
source: r#"try {
die "boom";
}
catch ($e) {
warn $e;
}
finally {
print "done";
}
"#,
},
EdgeCase {
id: "try.feature.gated",
description: "Try/catch with feature gating enabled.",
tags: &["try", "catch", "finally", "feature", "edge-case"],
source: r#"use feature 'try';
no warnings 'experimental::try';
try {
die "boom";
}
catch ($e) {
warn $e;
}
"#,
},
EdgeCase {
id: "defer.block",
description: "Defer blocks running on scope exit.",
tags: &["defer", "block", "edge-case"],
source: r#"use v5.36;
use feature 'defer';
no warnings 'experimental::defer';
sub cleanup {
defer { print "cleanup\n"; }
return 1;
}
"#,
},
EdgeCase {
id: "async.await",
description: "Async subroutine with await call.",
tags: &["async", "await", "feature", "edge-case"],
source: r#"use feature 'async_await';
no warnings 'experimental::async_await';
async sub fetch {
return await lookup();
}
sub lookup {
return 1;
}
my $result = await fetch();
"#,
},
EdgeCase {
id: "postfix.deref.slice",
description: "Postfix dereference with slice.",
tags: &["postfix", "dereference", "edge-case"],
source: r#"my $aref = [1, 2, 3];
my @slice = $aref->@[0, 2];
"#,
},
EdgeCase {
id: "postfix.deref.hash",
description: "Postfix dereference with hash expansion.",
tags: &["postfix", "dereference", "edge-case"],
source: r#"my $href = { a => 1, b => 2 };
my %copy = $href->%*;
my @keys = $href->@{qw(a b)};
"#,
},
EdgeCase {
id: "slice.hash.array",
description: "Array and hash slice notation with list context.",
tags: &["array", "hash", "list-context", "edge-case"],
source: r#"my @nums = (1, 2, 3, 4);
my @subset = @nums[1, 3];
my %map = (a => 1, b => 2, c => 3);
my @values = @map{qw(a c)};
"#,
},
EdgeCase {
id: "postfix.deref.code",
description: "Postfix dereference for a coderef.",
tags: &["postfix", "dereference", "edge-case"],
source: r#"my $code = sub { return 1; };
my $handler = $code->&*;
"#,
},
EdgeCase {
id: "class.field.method",
description: "Class with fields and method.",
tags: &["class", "field", "method", "edge-case"],
source: r#"class Point {
field $x :param = 0;
method get_x { return $x; }
}
"#,
},
EdgeCase {
id: "isa.operator",
description: "ISA operator with class checks.",
tags: &["isa", "operator", "edge-case", "class"],
source: r#"my $object = bless {}, "Thing";
if ($object isa Thing) {
print "ok";
}
"#,
},
EdgeCase {
id: "state.counter",
description: "State variable with initialization.",
tags: &["state", "edge-case"],
source: r#"sub counter($step = 1) {
state $count = 0;
return $count += $step;
}
"#,
},
EdgeCase {
id: "smartmatch.array",
description: "Smartmatch with array of roles.",
tags: &["smartmatch", "operator", "edge-case"],
source: r#"my @roles = qw(admin user);
if ("admin" ~~ @roles) {
print "has role";
}
"#,
},
EdgeCase {
id: "pack.unpack",
description: "Pack and unpack byte arrays.",
tags: &["pack", "unpack", "edge-case"],
source: r#"my $packed = pack("C*", 65, 66, 67);
my @bytes = unpack("C*", $packed);
"#,
},
EdgeCase {
id: "filetest.stack",
description: "Stacked filetest operators.",
tags: &["filetest", "edge-case"],
source: r#"if (-r -w -x $path) {
print "read write exec";
}
"#,
},
EdgeCase {
id: "filetest.handle",
description: "Filetest operator on a filehandle.",
tags: &["filetest", "file", "edge-case"],
source: r#"open my $fh, "<", "file.txt";
if (-t $fh) {
print "tty";
}
"#,
},
EdgeCase {
id: "ambiguous.slash",
description: "Division vs regex slash ambiguity.",
tags: &["regex", "operator", "ambiguous", "edge-case"],
source: r#"my $ratio = $a / $b;
my $match = $a =~ /$b/;
my $complex = $x / $y / $z;
my $regex = /$x\/$y/;
"#,
},
EdgeCase {
id: "indirect.object",
description: "Indirect object syntax for constructors.",
tags: &["method", "ambiguous", "parser-sensitive", "edge-case"],
source: r#"my $logger = new Logger "app.log";
my $time = new DateTime (year => 2024, month => 1, day => 1);
"#,
},
EdgeCase {
id: "special.vars",
description: "Special variables and sigil-heavy globals.",
tags: &["special-var", "variable", "edge-case"],
source: r#"my $program = $0;
my $error = $!;
my $status = $?;
my $count = @ARGV;
my $env_home = $ENV{HOME};
"#,
},
EdgeCase {
id: "special.constants",
description: "Special compile-time constants and __SUB__ usage.",
tags: &["special-var", "edge-case"],
source: r#"my $file = __FILE__;
my $line = __LINE__;
my $package = __PACKAGE__;
sub current_name { return __SUB__; }
"#,
},
EdgeCase {
id: "signal.handler",
description: "Signal handler assignment with SIG hash.",
tags: &["signal", "edge-case"],
source: r#"my $count = 0;
$SIG{INT} = sub { $count++; };
$SIG{TERM} = sub { die "shutdown"; };
"#,
},
EdgeCase {
id: "typeglob.alias",
description: "Typeglob aliasing and symbol table entries.",
tags: &["typeglob", "glob", "edge-case"],
source: r#"local *STDOUT = *DATA;
*Alias::printer = \&Other::printer;
"#,
},
EdgeCase {
id: "core.global.override",
description: "CORE::GLOBAL override for a builtin call.",
tags: &["builtin", "package", "edge-case"],
source: r#"BEGIN {
*CORE::GLOBAL::time = sub { 0 };
}
my $now = time();
"#,
},
EdgeCase {
id: "sort.block",
description: "Sort with comparison block.",
tags: &["sort", "list-context", "edge-case"],
source: r#"my @sorted = sort { $a <=> $b } @values;
"#,
},
EdgeCase {
id: "eval.string",
description: "String eval with error handling.",
tags: &["eval", "error", "edge-case"],
source: r#"my $code = "sub generated { return 42; }";
eval $code;
warn $@ if $@;
"#,
},
EdgeCase {
id: "sub.attribute",
description: "Subroutines with attributes.",
tags: &["subroutine", "method", "edge-case"],
source: r#"my $value = 1;
sub getter :lvalue { return $value; }
sub setter :method { $value = shift; }
"#,
},
EdgeCase {
id: "lexical.sub",
description: "Lexical subroutine declaration.",
tags: &["subroutine", "declaration", "feature", "edge-case"],
source: r#"use feature 'lexical_subs';
my sub helper ($x) { return $x + 1; }
my $value = helper(1);
"#,
},
EdgeCase {
id: "pod.basic",
description: "POD section with a simple header and cut.",
tags: &["pod", "edge-case"],
source: r#"=pod
=head1 NAME
Sample::Module
=cut
my $value = 1;
"#,
},
EdgeCase {
id: "vstring.literal",
description: "V-string version literals.",
tags: &["vstring", "version", "edge-case"],
source: r#"my $ver = v5.36.0;
my $min = v5.10;
"#,
},
EdgeCase {
id: "prototype.sub",
description: "Subroutine with prototype.",
tags: &["prototype", "subroutine", "edge-case"],
source: r#"sub sum ($$) { return $_[0] + $_[1]; }
"#,
},
EdgeCase {
id: "postfix.control",
description: "Postfix control flow with if/unless.",
tags: &["postfix", "if", "unless", "flow", "edge-case"],
source: r#"my $ready = 1;
print "go\n" if $ready;
warn "no\n" unless $ready;
"#,
},
EdgeCase {
id: "postfix.for",
description: "Postfix for loop with topic variable.",
tags: &["postfix", "for", "flow", "edge-case"],
source: r#"my @items = (1, 2, 3);
print $_ for @items;
"#,
},
EdgeCase {
id: "goto.label",
description: "Goto with label and conditional loop.",
tags: &["goto", "labels", "flow", "edge-case"],
source: r#"my $count = 0;
START:
$count++;
goto START if $count < 2;
"#,
},
EdgeCase {
id: "goto.label.simple",
description: "Goto with a plain label target and explicit statement terminators.",
tags: &["goto", "labels", "flow", "smoke", "edge-case"],
source: r#"goto FINISH;
FINISH:
1;
"#,
},
EdgeCase {
id: "use.feature.signatures",
description: "Feature and warning pragmas for signatures.",
tags: &["use", "feature", "signatures", "warnings", "edge-case"],
source: r#"use feature 'signatures';
no warnings 'experimental::signatures';
sub add ($x, $y) { return $x + $y; }
"#,
},
EdgeCase {
id: "constant.hash",
description: "use constant with a hash initializer.",
tags: &["constant", "use", "edge-case"],
source: r#"use constant { PI => 3.14159, E => 2.71828 };
my $area = PI * 2;
"#,
},
EdgeCase {
id: "builtin.truth",
description: "Builtin boolean helpers and predicates.",
tags: &["builtin", "feature", "edge-case"],
source: r#"use v5.36;
use builtin qw(true false is_bool);
my $flag = true;
my $ok = is_bool($flag);
"#,
},
EdgeCase {
id: "state.local.our",
description: "State, local, and our variable declarations.",
tags: &["state", "local", "our", "declaration", "edge-case"],
source: r#"our $global = 1;
local $global = 2;
sub tick {
state $count = 0;
return ++$count;
}
"#,
},
EdgeCase {
id: "phaser.blocks",
description: "Compile-time phase blocks with setup and teardown.",
tags: &["block", "statement", "edge-case"],
source: r#"BEGIN { $| = 1; }
UNITCHECK { my $setup = 1; }
CHECK { my $ok = 1; }
INIT { srand 42; }
END { print "done\n"; }
"#,
},
EdgeCase {
id: "regex.branch.reset",
description: "Regex branch reset groups with shared capture numbering.",
tags: &["regex", "branch-reset", "edge-case"],
source: r#"my $text = "ab";
if ($text =~ /(?|(a)(b)|(ab))/) {
print $1;
}
"#,
},
EdgeCase {
id: "regex.lookaround",
description: "Regex lookahead and lookbehind assertions.",
tags: &["regex", "assertion", "edge-case"],
source: r#"my $text = "foobar";
if ($text =~ /foo(?=bar)/) {
print "ahead";
}
if ($text =~ /(?<=foo)bar/) {
print "behind";
}
"#,
},
EdgeCase {
id: "regex.variable.lookbehind",
description: "Regex with a variable-length lookbehind.",
tags: &["regex", "assertion", "edge-case"],
source: r#"my $text = "foo123bar";
if ($text =~ /(?<=foo\d{1,3})bar/) {
print "match";
}
"#,
},
EdgeCase {
id: "regex.backtracking",
description: "Regex with nested quantifiers that can trigger backtracking.",
tags: &["regex", "edge-case", "parser-sensitive"],
source: r#"my $text = "aaaaaaaaab";
if ($text =~ /^(a+)+b$/) {
print "ok";
}
"#,
},
EdgeCase {
id: "regex.verbs",
description: "Regex with control verbs.",
tags: &["regex", "edge-case"],
source: r#"my $text = "abc";
if ($text =~ /a(*SKIP)(*FAIL)|abc/) {
print "match";
}
"#,
},
EdgeCase {
id: "regex.recursive",
description: "Regex with recursion.",
tags: &["regex", "edge-case"],
source: r#"my $text = "abc";
if ($text =~ /(a(?R)?c)/) {
print $1;
}
"#,
},
EdgeCase {
id: "regex.conditional",
description: "Regex conditional with a numbered capture branch.",
tags: &["regex", "edge-case"],
source: r#"my $text = "foobar";
if ($text =~ /(foo)?(?(1)bar|baz)/) {
print "ok";
}
"#,
},
EdgeCase {
id: "regex.conditional.named",
description: "Regex conditional using a named capture branch.",
tags: &["regex", "edge-case"],
source: r#"my $text = "foo";
if ($text =~ /(?<word>foo)(?(<word>)foo|bar)/) {
print "ok";
}
"#,
},
EdgeCase {
id: "regex.set.ops",
description: "Regex set operations with character class subtraction.",
tags: &["regex", "sets", "edge-case"],
source: r#"use v5.18;
my $text = "perl";
if ($text =~ /(?[ [a-z] - [aeiou] ])/ ) {
print "consonant";
}
"#,
},
EdgeCase {
id: "hash.block.ambiguity",
description: "Hash vs block ambiguity in function calls.",
tags: &["hash", "block", "ambiguous", "parser-sensitive", "edge-case"],
source: r#"sub handle { return 1; }
handle { key => 1 };
handle({ key => 1 });
"#,
},
EdgeCase {
id: "flipflop.operator",
description: "Flip-flop range operator in scalar context.",
tags: &["range", "flipflop", "operator", "flow", "edge-case"],
source: r#"my $hit = 0;
for my $i (1..6) {
$hit = 1 if $i == 2 .. $i == 4;
}
"#,
},
EdgeCase {
id: "autoload.destroy",
description: "AUTOLOAD and DESTROY subroutines for method fallback and cleanup.",
tags: &["autoload", "destructor", "method", "edge-case"],
source: r#"package Auto::Demo;
sub AUTOLOAD {
our $AUTOLOAD;
return $AUTOLOAD;
}
sub DESTROY { }
1;
"#,
},
EdgeCase {
id: "overload.stringify",
description: "Operator overloading for string and numeric contexts.",
tags: &["overload", "operator", "method", "edge-case"],
source: r#"package Counter;
use overload '""' => sub { $_[0]->{count} }, '0+' => sub { $_[0]->{count} }, fallback => 1;
sub new { bless { count => 1 }, shift }
1;
"#,
},
EdgeCase {
id: "symbolic.reference",
description: "Symbolic reference with disabled strict refs.",
tags: &["reference", "parser-sensitive", "edge-case"],
source: r#"no strict 'refs';
my $name = "value";
${$name} = 42;
my $value = ${"value"};
"#,
},
EdgeCase {
id: "data.section",
description: "DATA section with filehandle reads.",
tags: &["file", "io", "edge-case"],
source: r#"while (my $line = <DATA>) {
print $line;
}
__DATA__
alpha
beta
"#,
},
EdgeCase {
id: "readline.diamond",
description: "Diamond operator readline with eof check.",
tags: &["file", "io", "edge-case"],
source: r#"while (my $line = <>) {
last if eof;
print $line;
}
"#,
},
EdgeCase {
id: "open.layers",
description: "Open with PerlIO layer specification.",
tags: &["open", "layers", "perlio", "edge-case"],
source: r#"open my $fh, "<:encoding(UTF-8)", "file.txt" or die $!;
binmode $fh, ":raw";
"#,
},
EdgeCase {
id: "open.scalar.ref",
description: "Open a scalar reference as a filehandle.",
tags: &["open", "io", "edge-case"],
source: r#"my $data = "hello\nworld\n";
open my $fh, "<", \$data or die $!;
my $line = <$fh>;
"#,
},
EdgeCase {
id: "utf8.escape",
description: "UTF-8 pragma with escaped Unicode code points.",
tags: &["utf8", "unicode", "edge-case"],
source: r#"use utf8;
my $text = "na\x{EF}ve";
my $smile = "\x{1F600}";
"#,
},
EdgeCase {
id: "unicode.named.escape",
description: "Named Unicode escape with UTF-8 pragma.",
tags: &["unicode", "utf8", "edge-case"],
source: r#"use utf8;
my $text = "\N{LATIN SMALL LETTER E WITH ACUTE}";
"#,
},
EdgeCase {
id: "state.lexical.counter",
description: "State variable persists lexical value across subroutine calls.",
tags: &["state", "declaration", "edge-case"],
source: r#"use feature "state";
sub next_id {
state $id = 0;
return ++$id;
}
"#,
},
EdgeCase {
id: "end.section",
description: "END section with trailing content ignored by the parser.",
tags: &["end-section", "file", "edge-case"],
source: r#"print "before\n";
__END__
this should be ignored
"#,
},
EdgeCase {
id: "source.filter.simple",
description: "Source filter using Filter::Simple.",
tags: &["source-filter", "use", "edge-case", "parser-sensitive"],
source: r#"use Filter::Simple;
FILTER {
s/foo/bar/g;
}
my $text = "foo";
print $text;
"#,
},
EdgeCase {
id: "inline.c",
description: "Inline::C heredoc embedding C source.",
tags: &["inline", "xs", "ffi", "edge-case"],
source: r#"use Inline C => <<'END_C';
int add(int x, int y) {
return x + y;
}
END_C
my $sum = add(1, 2);
"#,
},
EdgeCase {
id: "inline.cpp",
description: "Inline::CPP heredoc embedding C++ source.",
tags: &["inline", "xs", "ffi", "edge-case"],
source: r#"use Inline CPP => <<'END_CPP';
#include <string>
class Greet {
public:
std::string hello(const char* name) {
return std::string("Hello, ") + name;
}
};
END_CPP
my $g = Greet->new();
"#,
},
EdgeCase {
id: "bareword.filehandle",
description: "Legacy bareword filehandle open/print/close.",
tags: &["file", "io", "legacy", "edge-case"],
source: r#"open FH, "<", "file.txt" or die $!;
print FH "ok\n";
close FH;
"#,
},
EdgeCase {
id: "lvalue.substr",
description: "Lvalue substring assignment.",
tags: &["lvalue", "string", "edge-case"],
source: r#"my $text = "foobar";
substr($text, 1, 3) = "OOO";
"#,
},
EdgeCase {
id: "mro.c3",
description: "Method resolution order pragma.",
tags: &["mro", "inheritance", "edge-case", "use"],
source: r#"use mro "c3";
our @ISA = ("Base");
sub method { return 1; }
"#,
},
EdgeCase {
id: "super.method",
description: "SUPER:: method dispatch from a subclass.",
tags: &["method", "inheritance", "edge-case", "package"],
source: r#"package Child;
use parent "Base";
sub new {
my $class = shift;
return $class->SUPER::new(@_);
}
1;
"#,
},
EdgeCase {
id: "object.can.does",
description: "Method introspection with can/DOES checks.",
tags: &["method", "can", "does", "edge-case"],
source: r#"package Widget;
sub new { bless {}, shift }
sub work { return 1; }
package main;
my $obj = Widget->new();
if ($obj->can("work")) {
$obj->work();
}
if ($obj->DOES("Role::Worker")) {
print "role";
}
"#,
},
EdgeCase {
id: "goto.sub",
description: "Goto to a subroutine for tail-call style dispatch.",
tags: &["goto", "subroutine", "edge-case"],
source: r#"sub helper { return 42; }
sub wrapper { goto &helper; }
my $value = wrapper();
"#,
},
EdgeCase {
id: "transliteration.y",
description: "Transliteration using the y/// alias.",
tags: &["transliteration", "tr", "edge-case"],
source: r#"my $text = "abc";
$text =~ y/a-z/A-Z/;
"#,
},
EdgeCase {
id: "variable.attribute.shared",
description: "Variable attribute using threads::shared.",
tags: &["attribute", "variable", "edge-case", "declaration"],
source: r#"use threads::shared;
my $counter :shared = 0;
"#,
},
];
static COMPLEX_DATA_STRUCTURE_CASES: [ComplexDataStructureCase; 32] = [
ComplexDataStructureCase {
id: "nested.hash.array",
description: "Nested hash/array structure.",
source: r#"my $data = {
users => [
{ id => 1, name => "Ada" },
{ id => 2, name => "Bob" },
],
flags => { active => 1, admin => 0 },
};
"#,
},
ComplexDataStructureCase {
id: "circular.reference",
description: "Self-referential hash.",
source: r#"my $node = {};
$node->{self} = $node;
"#,
},
ComplexDataStructureCase {
id: "blessed.object",
description: "Blessed hash reference.",
source: r#"my $obj = bless { name => "Widget", count => 3 }, "My::Class";
"#,
},
ComplexDataStructureCase {
id: "blessed.nested",
description: "Blessed hash with nested collections.",
source: r#"my $obj = bless {
name => "Widget",
tags => ["alpha", "beta"],
meta => { active => 1, retries => 2 },
}, "Widget::Thing";
"#,
},
ComplexDataStructureCase {
id: "mapped.records",
description: "Array of hash records created via map.",
source: r#"my @values = map { { id => $_, name => "item_$_" } } (1..5);
"#,
},
ComplexDataStructureCase {
id: "typeglob.alias",
description: "Typeglob aliasing and filehandle.",
source: r#"open my $fh, "<", "file.txt";
*ALIAS = *STDOUT;
"#,
},
ComplexDataStructureCase {
id: "typeglob.map",
description: "Hash containing typeglob handle references.",
source: r#"my $handles = {
out => *STDOUT,
err => *STDERR,
};
"#,
},
ComplexDataStructureCase {
id: "graph.refs",
description: "Graph-like structure with nested edges.",
source: r#"my $graph = {
nodes => [
{ id => 1, edges => [2, 3] },
{ id => 2, edges => [1] },
],
meta => { directed => 0 },
};
"#,
},
ComplexDataStructureCase {
id: "weakref.graph",
description: "Graph with weak reference edges.",
source: r#"use Scalar::Util 'weaken';
my $a = { id => "a" };
my $b = { id => "b" };
$a->{next} = $b;
$b->{prev} = $a;
weaken($b->{prev});
"#,
},
ComplexDataStructureCase {
id: "handlers.hash",
description: "Hash of handlers with coderefs.",
source: r#"my $handlers = {
on_ready => sub { return 1; },
on_error => sub { return 0; },
};
"#,
},
ComplexDataStructureCase {
id: "deep.nested.refs",
description: "Deeply nested references with arrays and hashes.",
source: r#"my $data = {
items => [
{ id => 1, children => [ { id => 2 }, { id => 3 } ] },
{ id => 4, children => [] },
],
meta => { count => 2 },
};
"#,
},
ComplexDataStructureCase {
id: "hash.special.keys",
description: "Hash with empty and spaced keys.",
source: r#"my $data = {
"" => 0,
" spaced key " => 1,
"0" => "zero",
};
"#,
},
ComplexDataStructureCase {
id: "unicode.escapes",
description: "Unicode escapes stored in scalars and hashes.",
source: r#"use utf8;
my $smile = "\x{1F600}";
my $text = "\x{4E16}\x{754C}";
my $data = {
emoji => $smile,
cjk => $text,
mixed => "$text $smile",
};
"#,
},
ComplexDataStructureCase {
id: "array.of.blessed",
description: "Array of blessed hash references.",
source: r#"my $objs = [
bless({ id => 1, label => "a" }, "Obj"),
bless({ id => 2, label => "b" }, "Obj"),
];
"#,
},
ComplexDataStructureCase {
id: "mixed.types",
description: "Array with mixed scalar and reference types.",
source: r#"my $data = [
1,
"two",
[3, 4],
{ five => 5 },
sub { return 6; },
];
"#,
},
ComplexDataStructureCase {
id: "array.self.ref",
description: "Array that contains a reference to itself.",
source: r#"my $list = [];
push @$list, $list;
"#,
},
ComplexDataStructureCase {
id: "blessed.array",
description: "Blessed array reference object.",
source: r#"my $obj = bless [1, 2, 3], "ArrayObj";
"#,
},
ComplexDataStructureCase {
id: "refs.in.hash",
description: "Hash with scalar references and nested collections.",
source: r#"my $value = 3;
my $data = {
value => \$value,
list => [1, 2, 3],
lookup => { a => 1 },
};
"#,
},
ComplexDataStructureCase {
id: "hash.with.undef",
description: "Hash with undef and falsey values.",
source: r#"my $data = {
ok => 1,
nope => 0,
maybe => undef,
note => "value",
};
"#,
},
ComplexDataStructureCase {
id: "dualvar.scalar",
description: "Scalar with dual numeric/string value.",
source: r#"use Scalar::Util 'dualvar';
my $value = dualvar(10, "ten");
"#,
},
ComplexDataStructureCase {
id: "version.object",
description: "Version object parsed from a v-string.",
source: r#"use version;
my $version = version->parse("v1.2.3");
"#,
},
ComplexDataStructureCase {
id: "regex.and.refs",
description: "Hash with compiled regex and scalar reference.",
source: r#"my $value = 10;
my $data = {
matcher => qr/^item_\d+$/i,
value_ref => \$value,
flags => [undef, 0, 1],
};
"#,
},
ComplexDataStructureCase {
id: "scalar.ref.chain",
description: "Nested scalar references.",
source: r#"my $value = 42;
my $ref1 = \$value;
my $ref2 = \$ref1;
"#,
},
ComplexDataStructureCase {
id: "tied.array",
description: "Tied array with queued values.",
source: r#"tie my @queue, "Tie::Array";
push @queue, "first";
my $item = $queue[0];
"#,
},
ComplexDataStructureCase {
id: "tied.hash",
description: "Tied hash with stored values.",
source: r#"tie my %cache, "Tie::StdHash";
$cache{foo} = 1;
my $value = $cache{foo};
"#,
},
ComplexDataStructureCase {
id: "closure.capture",
description: "Closure capturing a lexical variable.",
source: r#"my $count = 0;
my $next = sub { return ++$count; };
"#,
},
ComplexDataStructureCase {
id: "stash.entries",
description: "Symbol table stash reference with package entries.",
source: r#"package Stash::Demo;
our $VERSION = "0.01";
sub helper { return 1; }
my $stash = \%Stash::Demo::;
"#,
},
ComplexDataStructureCase {
id: "blessed.scalar.ref",
description: "Blessed scalar reference object.",
source: r#"my $value = 99;
my $obj = bless \$value, "ScalarObj";
"#,
},
ComplexDataStructureCase {
id: "blessed.coderef",
description: "Blessed coderef object.",
source: r#"my $handler = sub { return 1; };
my $obj = bless $handler, "Handler::Obj";
"#,
},
ComplexDataStructureCase {
id: "filehandle.in.hash",
description: "Hash containing a filehandle and metadata.",
source: r#"open my $fh, "<", "file.txt";
my $data = { handle => $fh, path => "file.txt" };
"#,
},
ComplexDataStructureCase {
id: "filehandle.array",
description: "Array containing filehandles and typeglob refs.",
source: r#"open my $fh, "<", "file.txt";
my $list = [$fh, \*STDIN, \*STDOUT];
"#,
},
ComplexDataStructureCase {
id: "tied.scalar",
description: "Tied scalar value.",
source: r#"tie my $counter, "Tie::Scalar";
$counter = 1;
"#,
},
];
pub fn edge_cases() -> &'static [EdgeCase] {
&EDGE_CASES
}
pub fn complex_data_structure_cases() -> &'static [ComplexDataStructureCase] {
&COMPLEX_DATA_STRUCTURE_CASES
}
pub fn get_complex_data_structure_tests() -> &'static [ComplexDataStructureCase] {
complex_data_structure_cases()
}
fn select_by_seed<T>(items: &[T], seed: u64) -> Option<&T> {
if items.is_empty() {
return None;
}
let idx = (seed % items.len() as u64) as usize;
items.get(idx)
}
fn case_has_any_tag(case: &EdgeCase, tags: &[&str]) -> bool {
case.tags.iter().any(|tag| tags.contains(tag))
}
fn case_has_all_tags(case: &EdgeCase, tags: &[&str]) -> bool {
tags.iter().all(|tag| case.tags.iter().any(|candidate| candidate == tag))
}
pub struct EdgeCaseGenerator;
impl EdgeCaseGenerator {
pub fn all_cases() -> &'static [EdgeCase] {
edge_cases()
}
pub fn by_tag(tag: &str) -> Vec<&'static EdgeCase> {
edge_cases().iter().filter(|case| case.tags.contains(&tag)).collect()
}
pub fn by_tags_any(tags: &[&str]) -> Vec<&'static EdgeCase> {
if tags.is_empty() {
return edge_cases().iter().collect();
}
edge_cases().iter().filter(|case| case_has_any_tag(case, tags)).collect()
}
pub fn by_tags_all(tags: &[&str]) -> Vec<&'static EdgeCase> {
if tags.is_empty() {
return edge_cases().iter().collect();
}
edge_cases().iter().filter(|case| case_has_all_tags(case, tags)).collect()
}
pub fn find(id: &str) -> Option<&'static EdgeCase> {
edge_cases().iter().find(|case| case.id == id)
}
pub fn sample(seed: u64) -> &'static EdgeCase {
select_by_seed(&EDGE_CASES, seed).unwrap_or(&EDGE_CASES[0])
}
pub fn sample_by_tag(tag: &str, seed: u64) -> Option<&'static EdgeCase> {
let matches = Self::by_tag(tag);
select_by_seed(&matches, seed).copied()
}
pub fn sample_by_tags_any(tags: &[&str], seed: u64) -> Option<&'static EdgeCase> {
let matches = Self::by_tags_any(tags);
select_by_seed(&matches, seed).copied()
}
pub fn sample_by_tags_all(tags: &[&str], seed: u64) -> Option<&'static EdgeCase> {
let matches = Self::by_tags_all(tags);
select_by_seed(&matches, seed).copied()
}
pub fn tags() -> Vec<&'static str> {
let mut tags: Vec<&'static str> =
edge_cases().iter().flat_map(|case| case.tags.iter().copied()).collect();
tags.sort();
tags.dedup();
tags
}
}
pub fn find_complex_case(id: &str) -> Option<&'static ComplexDataStructureCase> {
complex_data_structure_cases().iter().find(|case| case.id == id)
}
pub fn sample_complex_case(seed: u64) -> &'static ComplexDataStructureCase {
select_by_seed(&COMPLEX_DATA_STRUCTURE_CASES, seed).unwrap_or(&COMPLEX_DATA_STRUCTURE_CASES[0])
}
#[cfg(test)]
mod tests {
use super::*;
use perl_tdd_support::must_some;
use std::collections::HashSet;
#[test]
fn edge_cases_have_ids() {
assert!(edge_cases().iter().all(|case| !case.id.is_empty()));
}
#[test]
fn edge_cases_can_filter_by_tag() {
let heredocs = EdgeCaseGenerator::by_tag("heredoc");
assert!(!heredocs.is_empty());
}
#[test]
fn edge_cases_can_filter_by_any_tag() {
let matches = EdgeCaseGenerator::by_tags_any(&["regex", "heredoc"]);
assert!(!matches.is_empty());
}
#[test]
fn edge_cases_can_filter_by_all_tags() {
let matches = EdgeCaseGenerator::by_tags_all(&["regex", "regex-code"]);
assert!(matches.iter().any(|case| case.id == "regex.code"));
}
#[test]
fn edge_cases_filter_with_empty_tags_returns_all_cases() {
let any_matches = EdgeCaseGenerator::by_tags_any(&[]);
let all_matches = EdgeCaseGenerator::by_tags_all(&[]);
assert_eq!(any_matches.len(), edge_cases().len());
assert_eq!(all_matches.len(), edge_cases().len());
}
#[test]
fn edge_cases_filter_unknown_tags_returns_no_cases() {
assert!(EdgeCaseGenerator::by_tags_any(&["tag-does-not-exist"]).is_empty());
assert!(
EdgeCaseGenerator::by_tags_all(&["tag-does-not-exist", "still-missing"]).is_empty()
);
}
#[test]
fn edge_case_sample_by_tags_returns_none_when_no_matches_exist() {
assert!(EdgeCaseGenerator::sample_by_tags_any(&["tag-does-not-exist"], 7).is_none());
assert!(EdgeCaseGenerator::sample_by_tags_all(&["tag-does-not-exist"], 7).is_none());
}
#[test]
fn edge_case_tags_are_unique() {
let tags = EdgeCaseGenerator::tags();
let mut deduped = tags.clone();
deduped.sort();
deduped.dedup();
assert_eq!(tags, deduped);
}
#[test]
fn edge_case_ids_are_unique() {
let mut seen = HashSet::new();
for case in edge_cases() {
assert!(seen.insert(case.id), "Duplicate edge case id: {}", case.id);
}
}
#[test]
fn edge_case_sample_is_stable() {
let first = EdgeCaseGenerator::sample(7);
let second = EdgeCaseGenerator::sample(7);
assert_eq!(first.id, second.id);
}
#[test]
fn edge_case_sample_by_tag_matches() {
let case = must_some(EdgeCaseGenerator::sample_by_tag("regex", 3));
assert!(case.tags.contains(&"regex"));
}
#[test]
fn edge_case_lookup_by_id() {
let case = must_some(EdgeCaseGenerator::find("state.lexical.counter"));
assert!(case.tags.contains(&"state"));
}
#[test]
fn complex_case_lookup_by_id() {
let case = find_complex_case("nested.hash.array");
assert!(case.is_some());
}
#[test]
fn complex_case_sample_is_stable() {
let first = sample_complex_case(11);
let second = sample_complex_case(11);
assert_eq!(first.id, second.id);
}
#[test]
fn complex_case_ids_are_unique() {
let mut seen = HashSet::new();
for case in complex_data_structure_cases() {
assert!(seen.insert(case.id), "Duplicate complex case id: {}", case.id);
}
}
}