File Coverage

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

linestmtbrancondsubtimecode
1package App::Test::Generator::Mutation::ReturnUndef;
2
3
19
19
19
138475
23
191
use strict;
4
19
19
19
26
20
337
use warnings;
5
19
19
19
27
15
44
use parent 'App::Test::Generator::Mutation::Base';
6
19
19
19
629
42
149
use App::Test::Generator::Mutant;
7
19
19
19
26
20
6725
use PPI;
8
9our $VERSION = '0.46';
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.46

=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
62sub applies_to {
63
71
8364
        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
8188
30113
                my $node = $_[1];
75
8188
9400
                return 0 unless $node->isa('PPI::Statement::Break');
76
198
211
                my $first = $node->schild(0) or return 0;
77
198
1131
                return $first->content eq 'return';
78
71
280
        }) || [];
79
80
71
71
502
194
        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
168sub mutate {
169
104
47067
        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
8868
32430
                my $node = $_[1];
175                # Match Break nodes that are specifically return statements
176
8868
10075
                return 0 unless $node->isa('PPI::Statement::Break');
177
240
246
                my $first = $node->schild(0) or return 0;
178
240
1330
                return $first->content eq 'return';
179
104
321
        }) || [];
180
181
104
658
        my @mutants;
182
183
104
104
140
183
        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
240
335
                my @expr = _return_expr_span($ret);
189
240
262
                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
234
476
                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
233
294
                my $line = $ret->location->[0];
198
233
9826
                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
233
1513
                my $id = "RETURN_UNDEF_${line}_${col}";
203
204
233
191
                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
17
                                        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
35
                                                }) || [];
227
12
12
79
14
                                        for my $ret (@{$rets}) {
228
13
554
                                                next unless $ret->line_number   == $line;
229
12
2620
                                                next unless $ret->column_number == $col;
230
231
12
115
                                                my @expr = _return_expr_span($ret);
232
12
16
                                                last unless @expr;
233
12
37
                                                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
18
                                                $expr[0]->insert_before(PPI::Token::Word->new('undef'));
242
12
415
                                                $_->remove for @expr;
243
12
387
                                                last;
244                                        }
245                                },
246
233
387
                        );
247                };
248
249                # If the Mutant construction fails, report clearly rather than
250                # silently dropping the mutant from the results
251
233
504
                if($@ || !$mutant) {
252
1
20
                        warn "Failed to construct mutant $id: $@" if $@;
253
1
2
                        next;
254                }
255
256
232
294
                push @mutants, $mutant;
257        }
258
259
104
211
        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# --------------------------------------------------
275sub _return_expr_span {
276
256
8568
        my ($ret) = @_;
277
278
256
323
        my @children = $ret->schildren;
279
256
1544
        shift @children;
280
281
256
761
        if(@children && $children[-1]->isa('PPI::Token::Structure') && $children[-1]->content eq ';') {
282
232
505
                pop @children;
283        }
284
285
256
319
        for my $i (0 .. $#children) {
286
448
405
                my $child = $children[$i];
287
448
686
                next unless $child->isa('PPI::Token::Word');
288
20
33
                next unless $child->content =~ /^(?:if|unless|while|until|for|foreach)$/;
289
11
47
                @children = @children[0 .. $i - 1];
290
11
11
                last;
291        }
292
293
256
276
        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
3101;