| File: | blib/lib/App/Test/Generator/Mutation/ReturnUndef.pm |
| Coverage: | 92.9% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::Test::Generator::Mutation::ReturnUndef; | |||||
| 2 | ||||||
| 3 | 14 14 14 | 134848 12 181 | use strict; | |||
| 4 | 14 14 14 | 17 12 279 | use warnings; | |||
| 5 | 14 14 14 | 23 15 27 | use parent 'App::Test::Generator::Mutation::Base'; | |||
| 6 | 14 14 14 | 503 9 118 | use App::Test::Generator::Mutant; | |||
| 7 | 14 14 14 | 24 24 4909 | use PPI; | |||
| 8 | ||||||
| 9 | our $VERSION = '0.41'; | |||||
| 10 | ||||||
| 11 - 60 | =head1 NAME
App::Test::Generator::Mutation::ReturnUndef - Replace return expressions
with undef to expose missing undef-return checks in the test suite
=head1 VERSION
Version 0.41
=head1 METHODS
=head2 applies_to
Return true if the given document contains at least one return
statement this mutation strategy could mutate. Used by
L<App::Test::Generator::Mutator> to pre-filter strategies before
calling C<mutate>, so a document with nothing to mutate skips the
walk entirely.
my $applies = $mutation->applies_to($doc);
=head3 Arguments
=over 4
=item * C<$doc>
A L<PPI::Document> object to inspect.
=back
=head3 Returns
True if the document contains a C<return> statement (PPI::Statement::Break
whose first token is C<return>), false otherwise.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::ReturnUndef' },
doc => { type => OBJECT, isa => 'PPI::Document' },
}
=head4 output
{ type => SCALAR }
=cut | |||||
| 61 | ||||||
| 62 | sub applies_to { | |||||
| 63 | 54 | 7765 | my ($self, $doc) = @_; | |||
| 64 | ||||||
| 65 | # PPI >= 1.270 classifies return as PPI::Statement::Break rather | |||||
| 66 | # than the dedicated PPI::Statement::Return class -- scan the whole | |||||
| 67 | # document for at least one qualifying return statement. This must | |||||
| 68 | # match the document-level pre-filter contract used by | |||||
| 69 | # Mutator::generate_mutants (and documented in Mutation::Base) rather | |||||
| 70 | # than testing a single node, otherwise every call from | |||||
| 71 | # generate_mutants would see $doc itself, which is never a | |||||
| 72 | # PPI::Statement::Break, and mutate() would never run. | |||||
| 73 | my $returns = $doc->find(sub { | |||||
| 74 | 5545 | 20616 | my $node = $_[1]; | |||
| 75 | 5545 | 6255 | return 0 unless $node->isa('PPI::Statement::Break'); | |||
| 76 | 158 | 138 | my $first = $node->schild(0) or return 0; | |||
| 77 | 158 | 828 | return $first->content eq 'return'; | |||
| 78 | 54 | 148 | }) || []; | |||
| 79 | ||||||
| 80 | 54 54 | 375 102 | return @{$returns} ? 1 : 0; | |||
| 81 | } | |||||
| 82 | ||||||
| 83 - 166 | =head2 mutate
Walk a PPI document and generate one mutant for each non-bare return
statement, replacing its expression with C<undef>. For example,
C<return $result> becomes C<return undef>.
Bare C<return;> statements are skipped because they already return
undef - mutating them would produce a redundant mutant that can never
be killed.
my $mutation = App::Test::Generator::Mutation::ReturnUndef->new;
my $doc = PPI::Document->new(\$source);
my @mutants = $mutation->mutate($doc);
for my $m (@mutants) {
print $m->id, ': ', $m->description, "\n";
}
=head3 Arguments
=over 4
=item * C<$self>
An instance of C<App::Test::Generator::Mutation::ReturnUndef>.
=item * C<$doc>
A L<PPI::Document> object representing the parsed source to mutate.
The document is not modified by this method.
=back
=head3 Returns
A list of L<App::Test::Generator::Mutant> objects, one per qualifying
return statement found in the document. Returns an empty list if no
non-bare return statements are found.
Each mutant carries a C<transform> closure that when called with a
fresh L<PPI::Document> copy will replace the targeted return expression
with the literal C<undef>.
=head3 Notes
Mutant IDs include both line and column number to ensure uniqueness
when multiple return statements appear in the same source file.
Only return statements with an expression child are mutated - bare
C<return;> statements are skipped as they already return undef.
Each mutant's optional C<context> field is set to C<conditional> if
the return statement sits inside (or is itself the keyword of) an
C<if>/C<unless>/C<while>/C<until> compound statement, or C<statement>
otherwise; its C<line_content> field holds the raw source text of the
mutated line. Both are consumed by
L<App::Test::Generator::Mutator>'s fast-mode dedup.
=head3 API specification
=head4 input
{
self => {
type => OBJECT,
isa => 'App::Test::Generator::Mutation::ReturnUndef',
},
doc => {
type => OBJECT,
isa => 'PPI::Document',
},
}
=head4 output
{
type => ARRAYREF,
elements => {
type => OBJECT,
isa => 'App::Test::Generator::Mutant',
},
}
=cut | |||||
| 167 | ||||||
| 168 | sub mutate { | |||||
| 169 | 87 | 46875 | my ($self, $doc) = @_; | |||
| 170 | ||||||
| 171 | # PPI >= 1.270 classifies return statements as PPI::Statement::Break | |||||
| 172 | # rather than PPI::Statement::Return -- use a custom predicate | |||||
| 173 | my $returns = $doc->find(sub { | |||||
| 174 | 6225 | 23309 | my $node = $_[1]; | |||
| 175 | # Match Break nodes that are specifically return statements | |||||
| 176 | 6225 | 6920 | return 0 unless $node->isa('PPI::Statement::Break'); | |||
| 177 | 200 | 178 | my $first = $node->schild(0) or return 0; | |||
| 178 | 200 | 1031 | return $first->content eq 'return'; | |||
| 179 | 87 | 224 | }) || []; | |||
| 180 | ||||||
| 181 | 87 | 514 | my @mutants; | |||
| 182 | ||||||
| 183 | 87 87 | 70 159 | for my $ret (@{$returns}) { | |||
| 184 | # Skip bare return statements â they already return undef | |||||
| 185 | # so mutating them would produce a redundant mutant that | |||||
| 186 | # can never be killed by any meaningful test. Also skips | |||||
| 187 | # bare returns with only a postfix conditional/loop modifier. | |||||
| 188 | 200 | 198 | my @expr = _return_expr_span($ret); | |||
| 189 | 200 | 183 | next unless @expr; | |||
| 190 | ||||||
| 191 | # Skip a lone structure node (e.g. return ($x, $y) gives a | |||||
| 192 | # single PPI::Structure::List child) â nothing simple to splice | |||||
| 193 | 194 | 365 | next if @expr == 1 && !$expr[0]->isa('PPI::Token'); | |||
| 194 | ||||||
| 195 | # Capture location so the transform closure targets the | |||||
| 196 | # exact statement rather than the first match on that line | |||||
| 197 | 193 | 204 | my $line = $ret->location->[0]; | |||
| 198 | 193 | 9194 | my $col = $ret->location->[1]; | |||
| 199 | ||||||
| 200 | # Build a unique ID from line and column so multiple return | |||||
| 201 | # statements in the same file never collide | |||||
| 202 | 193 | 1222 | my $id = "RETURN_UNDEF_${line}_${col}"; | |||
| 203 | ||||||
| 204 | 193 | 142 | my $mutant = eval { | |||
| 205 | App::Test::Generator::Mutant->new( | |||||
| 206 | id => $id, | |||||
| 207 | group => "RETURN_UNDEF:$line", | |||||
| 208 | description => 'Replace return expression with undef', | |||||
| 209 | original => $ret->content(), | |||||
| 210 | line => $line, | |||||
| 211 | type => 'return', | |||||
| 212 | context => $self->_in_conditional($ret) ? 'conditional' : 'statement', | |||||
| 213 | line_content => $self->_line_content($doc, $line), | |||||
| 214 | ||||||
| 215 | # The transform closure captures line and col so it | |||||
| 216 | # targets precisely the right return statement in the | |||||
| 217 | # document copy it receives at test time | |||||
| 218 | transform => sub { | |||||
| 219 | 12 | 12 | my $doc = $_[0]; | |||
| 220 | # PPI >= 1.270 uses PPI::Statement::Break for return | |||||
| 221 | my $rets = $doc->find(sub { | |||||
| 222 | my $node = $_[1]; | |||||
| 223 | return 0 unless $node->isa('PPI::Statement::Break'); | |||||
| 224 | my $first = $node->schild(0) or return 0; | |||||
| 225 | return $first->content eq 'return'; | |||||
| 226 | 12 | 32 | }) || []; | |||
| 227 | 12 12 | 71 13 | for my $ret (@{$rets}) { | |||
| 228 | 13 | 539 | next unless $ret->line_number == $line; | |||
| 229 | 12 | 2577 | next unless $ret->column_number == $col; | |||
| 230 | ||||||
| 231 | 12 | 124 | my @expr = _return_expr_span($ret); | |||
| 232 | 12 | 14 | last unless @expr; | |||
| 233 | 12 | 28 | last if @expr == 1 && !$expr[0]->isa('PPI::Token'); | |||
| 234 | ||||||
| 235 | # Replace the whole expression span with a single | |||||
| 236 | # 'undef' token rather than just its first token -- | |||||
| 237 | # $self->{x} is three significant children (Symbol, | |||||
| 238 | # Operator, Structure) and replacing only the | |||||
| 239 | # leading $self produced the broken mutant | |||||
| 240 | # 'return undef->{x};' | |||||
| 241 | 12 | 32 | $expr[0]->insert_before(PPI::Token::Word->new('undef')); | |||
| 242 | 12 | 318 | $_->remove for @expr; | |||
| 243 | 12 | 336 | last; | |||
| 244 | } | |||||
| 245 | }, | |||||
| 246 | 193 | 249 | ); | |||
| 247 | }; | |||||
| 248 | ||||||
| 249 | # If the Mutant construction fails, report clearly rather than | |||||
| 250 | # silently dropping the mutant from the results | |||||
| 251 | 193 | 962 | if($@ || !$mutant) { | |||
| 252 | 1 | 8 | warn "Failed to construct mutant $id: $@" if $@; | |||
| 253 | 1 | 502 | next; | |||
| 254 | } | |||||
| 255 | ||||||
| 256 | 192 | 225 | push @mutants, $mutant; | |||
| 257 | } | |||||
| 258 | ||||||
| 259 | 87 | 155 | return @mutants; | |||
| 260 | } | |||||
| 261 | ||||||
| 262 | # -------------------------------------------------- | |||||
| 263 | # Purpose: identify the PPI elements making up the expression | |||||
| 264 | # being returned by a 'return' statement, excluding | |||||
| 265 | # the leading 'return' keyword, the trailing statement | |||||
| 266 | # terminator, and any postfix conditional/loop modifier | |||||
| 267 | # (if/unless/while/until/for/foreach) and its condition. | |||||
| 268 | # Entry: a PPI::Statement::Break node already confirmed to be | |||||
| 269 | # a 'return' statement. | |||||
| 270 | # Exit: a list of the significant child elements making up | |||||
| 271 | # the return expression, or an empty list for a bare | |||||
| 272 | # return (with or without a postfix modifier). | |||||
| 273 | # Side effects: none. | |||||
| 274 | # -------------------------------------------------- | |||||
| 275 | sub _return_expr_span { | |||||
| 276 | 216 | 8458 | my ($ret) = @_; | |||
| 277 | ||||||
| 278 | 216 | 257 | my @children = $ret->schildren; | |||
| 279 | 216 | 1207 | shift @children; | |||
| 280 | ||||||
| 281 | 216 | 554 | if(@children && $children[-1]->isa('PPI::Token::Structure') && $children[-1]->content eq ';') { | |||
| 282 | 210 | 421 | pop @children; | |||
| 283 | } | |||||
| 284 | ||||||
| 285 | 216 | 236 | for my $i (0 .. $#children) { | |||
| 286 | 353 | 269 | my $child = $children[$i]; | |||
| 287 | 353 | 509 | next unless $child->isa('PPI::Token::Word'); | |||
| 288 | 18 | 21 | next unless $child->content =~ /^(?:if|unless|while|until|for|foreach)$/; | |||
| 289 | 11 | 40 | @children = @children[0 .. $i - 1]; | |||
| 290 | 11 | 11 | last; | |||
| 291 | } | |||||
| 292 | ||||||
| 293 | 216 | 218 | return @children; | |||
| 294 | } | |||||
| 295 | ||||||
| 296 - 308 | =head1 AUTHOR Nigel Horne, C<< <njh at nigelhorne.com> >> =head1 LICENCE AND COPYRIGHT Copyright 2026 Nigel Horne. Usage is subject to the terms of GPL2. If you use it, please let me know. =cut | |||||
| 309 | ||||||
| 310 | 1; | |||||