File Coverage

File:blib/lib/App/Test/Generator/Mutation/ReturnUndef.pm
Coverage:81.8%

linestmtbrancondsubtimecode
1package App::Test::Generator::Mutation::ReturnUndef;
2
3
10
10
10
135378
8
125
use strict;
4
10
10
10
16
7
175
use warnings;
5
10
10
10
17
6
27
use parent 'App::Test::Generator::Mutation::Base';
6
10
10
10
437
10
69
use App::Test::Generator::Mutant;
7
10
10
10
13
8
2847
use PPI;
8
9our $VERSION = '0.36';
10
11 - 57
=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.36

=head1 METHODS

=head2 applies_to

Return true if this mutation strategy applies to the given PPI node.
Used by the mutation framework to pre-filter nodes before calling
C<mutate>.

    my $applies = $mutation->applies_to($node);

=head3 Arguments

=over 4

=item * C<$node>

A L<PPI::Element> node to test.

=back

=head3 Returns

True if the node is a C<PPI::Statement::Return>, false otherwise.

=head3 API specification

=head4 input

    {
        self => { type => OBJECT, isa => 'App::Test::Generator::Mutation::ReturnUndef' },
        node => { type => OBJECT, isa => 'PPI::Element' },
    }

=head4 output

    { type => SCALAR }

=cut
58
59sub applies_to {
60
8
8513
        my ($self, $node) = @_;
61
62        # PPI >= 1.270 classifies return as PPI::Statement::Break
63        # rather than the dedicated PPI::Statement::Return class
64
8
22
        return 0 unless $node->isa('PPI::Statement::Break');
65
66        # Confirm it is specifically a return statement and not
67        # last, next, or redo which are also PPI::Statement::Break
68
3
3
        my $first = $node->schild(0) or return 0;
69
3
19
        return $first->content eq 'return';
70}
71
72 - 148
=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.

=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
149
150sub mutate {
151
66
38852
        my ($self, $doc) = @_;
152
153        # PPI >= 1.270 classifies return statements as PPI::Statement::Break
154        # rather than PPI::Statement::Return -- use a custom predicate
155        my $returns = $doc->find(sub {
156
4633
17760
                my $node = $_[1];
157                # Match Break nodes that are specifically return statements
158
4633
5384
                return 0 unless $node->isa('PPI::Statement::Break');
159
155
141
                my $first = $node->schild(0) or return 0;
160
155
818
                return $first->content eq 'return';
161
66
186
        }) || [];
162
163
66
396
        my @mutants;
164
165
66
66
51
80
        for my $ret (@{$returns}) {
166                # Skip bare return statements — they already return undef
167                # so mutating them would produce a redundant mutant that
168                # can never be killed by any meaningful test
169
155
143
                my $expr = $ret->schild(1) or next;
170
171                # Skip bare semicolon — PPI may include the statement
172                # terminator as a significant child on bare returns
173
155
1143
                next if $expr->isa('PPI::Token::Structure') && $expr->content eq ';';
174                # Skip structure nodes (e.g. return ($x, $y) gives a
175                # PPI::Structure::List) — we can only mutate token expressions
176
153
172
                next unless $expr->isa('PPI::Token');
177
178                # Skip postfix conditionals — replacing 'unless ...' with undef is invalid syntax
179
152
201
                next if $expr->isa('PPI::Token::Word') && $expr->content =~ /^(?:if|unless|while|until|for|foreach)$/;
180
181                # Capture location so the transform closure targets the
182                # exact statement rather than the first match on that line
183
148
155
                my $line = $ret->location->[0];
184
148
7079
                my $col  = $ret->location->[1];
185
186                # Build a unique ID from line and column so multiple return
187                # statements in the same file never collide
188
148
893
                my $id = "RETURN_UNDEF_${line}_${col}";
189
190
148
101
                my $mutant = eval {
191                        App::Test::Generator::Mutant->new(
192                                id          => $id,
193                                group       => "RETURN_UNDEF:$line",
194                                description => 'Replace return expression with undef',
195                                original    => $ret->content(),
196                                line        => $line,
197                                type        => 'return',
198
199                                # The transform closure captures line and col so it
200                                # targets precisely the right return statement in the
201                                # document copy it receives at test time
202                                transform => sub {
203
10
14
                                        my $doc  = $_[0];
204                                        # PPI >= 1.270 uses PPI::Statement::Break for return
205                                        my $rets = $doc->find(sub {
206                                                my $node = $_[1];
207                                                return 0 unless $node->isa('PPI::Statement::Break');
208                                                my $first = $node->schild(0) or return 0;
209                                                return $first->content eq 'return';
210
10
28
                                                }) || [];
211
10
10
56
11
                                        for my $ret (@{$rets}) {
212
11
546
                                                next unless $ret->line_number   == $line;
213
10
2111
                                                next unless $ret->column_number == $col;
214
10
89
                                                my $expr = $ret->schild(1) or last;
215
216                                                # Skip bare semicolon — already returns undef
217
10
88
                                                next if $expr->isa('PPI::Token::Structure') && $expr->content eq ';';
218
219                                                # Skip structure nodes (e.g. PPI::Structure::List from
220                                                # return ($x, $y)) — set_content only exists on tokens
221
10
13
                                                next unless $expr->isa('PPI::Token');
222
223                                                # Skip postfix conditionals — replacing 'unless ...' with undef is invalid syntax
224
10
22
                                                next if $expr->isa('PPI::Token::Word') && $expr->content =~ /^(?:if|unless|while|until|for|foreach)$/;
225
226
10
17
                                                $expr->set_content('undef');
227
10
20
                                                last;
228                                        }
229                                },
230
148
176
                        );
231                };
232
233                # If the Mutant construction fails, report clearly rather than
234                # silently dropping the mutant from the results
235
148
253
                if($@ || !$mutant) {
236
0
0
                        warn "Failed to construct mutant $id: $@" if $@;
237
0
0
                        next;
238                }
239
240
148
150
                push @mutants, $mutant;
241        }
242
243
66
145
        return @mutants;
244}
245
246 - 258
=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
259
2601;