| File: | blib/lib/App/Test/Generator/Analyzer/Complexity.pm |
| Coverage: | 98.3% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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 | # -------------------------------------------------- | |||||
| 10 | Readonly 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 | # -------------------------------------------------- | |||||
| 17 | Readonly my $LOW_THRESHOLD => 3; | |||||
| 18 | Readonly my $HIGH_THRESHOLD => 7; | |||||
| 19 | ||||||
| 20 | # -------------------------------------------------- | |||||
| 21 | # Complexity level labels | |||||
| 22 | # -------------------------------------------------- | |||||
| 23 | Readonly my $LEVEL_LOW => 'low'; | |||||
| 24 | Readonly my $LEVEL_MODERATE => 'moderate'; | |||||
| 25 | Readonly my $LEVEL_HIGH => 'high'; | |||||
| 26 | ||||||
| 27 | # -------------------------------------------------- | |||||
| 28 | # Keywords that introduce branching decision points | |||||
| 29 | # -------------------------------------------------- | |||||
| 30 | Readonly 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 | # -------------------------------------------------- | |||||
| 37 | Readonly my @EXCEPTION_TOKENS => qw( | |||||
| 38 | die croak confess try catch eval | |||||
| 39 | ); | |||||
| 40 | ||||||
| 41 | our $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 | ||||||
| 162 | sub 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 | ||||||
| 239 | 1; | |||||