File Coverage

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

linestmtbrancondsubtimecode
1package 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
9our $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
62sub 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
168sub 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# --------------------------------------------------
275sub _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
3101;