File Coverage

File:blib/lib/App/Test/Generator/Analyzer/Complexity.pm
Coverage:98.3%

linestmtbrancondsubtimecode
1package App::Test::Generator::Analyzer::Complexity;
2
3
24
24
24
65988
21
359
use strict;
4
24
24
24
39
15
400
use warnings;
5
24
24
24
34
17
5889
use Readonly;
6
7# --------------------------------------------------
8# Base cyclomatic complexity score before any analysis
9# --------------------------------------------------
10Readonly my $CYCLOMATIC_BASE => 1;
11
12# --------------------------------------------------
13# Complexity level thresholds — scores at or below
14# LOW_THRESHOLD are low, at or below HIGH_THRESHOLD
15# are moderate, above HIGH_THRESHOLD are high
16# --------------------------------------------------
17Readonly my $LOW_THRESHOLD  => 3;
18Readonly my $HIGH_THRESHOLD => 7;
19
20# --------------------------------------------------
21# Complexity level labels
22# --------------------------------------------------
23Readonly my $LEVEL_LOW      => 'low';
24Readonly my $LEVEL_MODERATE => 'moderate';
25Readonly my $LEVEL_HIGH     => 'high';
26
27# --------------------------------------------------
28# Keywords that introduce branching decision points
29# --------------------------------------------------
30Readonly my @BRANCH_TOKENS => qw(
31        if elsif unless for foreach while until given when
32);
33
34# --------------------------------------------------
35# Keywords that introduce exception or error paths
36# --------------------------------------------------
37Readonly my @EXCEPTION_TOKENS => qw(
38        die croak confess try catch eval
39);
40
41our $VERSION = '0.36';
42
43 - 82
=head1 VERSION

Version 0.36

=head1 DESCRIPTION

Analyses the source body of a method and produces a complexity report
including cyclomatic score, branching points, early returns, exception
paths, and nesting depth. Used by L<App::Test::Generator> to guide test
planning — higher complexity methods are prioritised for more thorough
test generation.

=head2 new

Construct a new Complexity analyser.

    my $analyser = App::Test::Generator::Analyzer::Complexity->new;

=head3 Arguments

None.

=head3 Returns

A blessed hashref.

=head3 API specification

=head4 input

    {}

=head4 output

    {
        type => OBJECT,
        isa  => 'App::Test::Generator::Analyzer::Complexity',
    }

=cut
83
84
310
119758
sub new { bless {}, shift }
85
86 - 160
=head2 analyze

Analyse the source of a method and return a complexity report hashref.

    my $analyser = App::Test::Generator::Analyzer::Complexity->new;
    my $report   = $analyser->analyze($method);

    printf "Cyclomatic score: %d\n", $report->{cyclomatic_score};
    printf "Complexity level: %s\n", $report->{complexity_level};

=head3 Arguments

=over 4

=item * C<$method>

An L<App::Test::Generator::Model::Method> object. The method source is
read via C<source()>.

=back

=head3 Returns

A hashref with the following keys:

=over 4

=item * C<cyclomatic_score> — integer starting at 1, incremented for
each branching point, logical operator, early return, and exception path.

=item * C<branching_points> — count of branching keywords found.

=item * C<early_returns> — number of C<return> statements beyond the
first (each additional return adds a path).

=item * C<exception_paths> — count of exception-related keywords found.

=item * C<nesting_depth> — maximum brace nesting depth observed.

=item * C<complexity_level> — one of C<low>, C<moderate>, or C<high>
based on the cyclomatic score.

=back

=head3 Notes

Nesting depth is computed by naive brace counting and will be
inaccurate if the source contains braces inside strings or regexes.
This is a known limitation and is acceptable for dashboard display
purposes.

=head3 API specification

=head4 input

    {
        self   => { type => OBJECT, isa => 'App::Test::Generator::Analyzer::Complexity' },
        method => { type => OBJECT, isa => 'App::Test::Generator::Model::Method' },
    }

=head4 output

    {
        type => HASHREF,
        keys => {
            cyclomatic_score  => { type => SCALAR },
            branching_points  => { type => SCALAR },
            early_returns     => { type => SCALAR },
            exception_paths   => { type => SCALAR },
            nesting_depth     => { type => SCALAR },
            complexity_level  => { type => SCALAR },
        },
    }

=cut
161
162sub analyze {
163
311
8141
        my ($self, $method) = @_;
164
165        # The method argument is a raw hashref from SchemaExtractor,
166        # not a Model::Method object — access the body key directly
167
311
332
        my $body = $method->{body} // '';
168
169
311
454
        my %result = (
170                cyclomatic_score => $CYCLOMATIC_BASE,
171                branching_points => 0,
172                early_returns    => 0,
173                exception_paths  => 0,
174                nesting_depth    => 0,
175        );
176
177        # --------------------------------------------------
178        # Count branching keywords — each one introduces a
179        # new decision point that increases cyclomatic complexity
180        # --------------------------------------------------
181
311
1308
        for my $token (@BRANCH_TOKENS) {
182
2799
6615
                my $count = () = $body =~ /\b$token\b/g;
183
2799
12427
                $result{branching_points} += $count;
184
2799
2790
                $result{cyclomatic_score} += $count;
185        }
186
187        # Logical operators also introduce implicit branches
188
311
907
        my $logic_count = () = $body =~ /&&|\|\||\?/g;
189
311
261
        $result{cyclomatic_score} += $logic_count;
190
191        # --------------------------------------------------
192        # Early returns — each return beyond the first adds
193        # an additional exit path through the method
194        # --------------------------------------------------
195
311
540
        my $return_count = () = $body =~ /\breturn\b/g;
196
311
402
        $result{early_returns}    = $return_count > 1 ? $return_count - 1 : 0;
197
311
273
        $result{cyclomatic_score} += $result{early_returns};
198
199        # --------------------------------------------------
200        # Exception paths — die/croak/eval etc. each introduce
201        # a path that must be tested separately
202        # --------------------------------------------------
203
311
325
        for my $token (@EXCEPTION_TOKENS) {
204
1866
3983
                my $count = () = $body =~ /\b$token\b/g;
205
1866
7043
                $result{exception_paths} += $count;
206
1866
1734
                $result{cyclomatic_score} += $count;
207        }
208
209        # --------------------------------------------------
210        # Nesting depth — count brace depth by scanning chars.
211        # NOTE: this is naive and will overcount if braces
212        # appear inside strings or regexes. Acceptable for
213        # dashboard display purposes.
214        # --------------------------------------------------
215
311
545
        my $depth     = 0;
216
311
209
        my $max_depth = 0;
217
311
1329
        for my $char (split //, $body) {
218
21272
15630
                if($char eq '{') {
219
451
284
                        $depth++;
220
451
411
                        $max_depth = $depth if $depth > $max_depth;
221                } elsif($char eq '}') {
222
451
423
                        $depth-- if $depth > 0;
223                }
224        }
225
311
705
        $result{nesting_depth} = $max_depth;
226
227        # --------------------------------------------------
228        # Classify complexity level based on cyclomatic score
229        # --------------------------------------------------
230
311
266
        my $score = $result{cyclomatic_score};
231        $result{complexity_level} =
232
311
380
                $score <= $LOW_THRESHOLD  ? $LEVEL_LOW      :
233                $score <= $HIGH_THRESHOLD ? $LEVEL_MODERATE :
234                                            $LEVEL_HIGH;
235
236
311
1558
        return \%result;
237}
238
2391;