File Coverage

blib/lib/Perl/Critic/Policy/CognitiveComplexity/ProhibitExcessCognitiveComplexity.pm
Criterion Covered Total %
statement 90 91 98.9
branch 27 28 96.4
condition 21 21 100.0
subroutine 21 22 95.4
pod 4 5 80.0
total 163 167 97.6


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::CognitiveComplexity::ProhibitExcessCognitiveComplexity;
2 13     13   1747652 use strict;
  13         19  
  13         337  
3 13     13   61 use warnings;
  13         18  
  13         279  
4              
5 13     13   464 use Readonly;
  13         2525  
  13         501  
6 13     13   51 use Readonly qw (Scalar);
  13         13  
  13         537  
7 13     13   640 use Perl::Critic::Utils qw{ :severities :classification :ppi };
  13         80526  
  13         583  
8 13     13   9534 use Perl::Critic::Violation;
  13         161070  
  13         343  
9 13     13   64 use base 'Perl::Critic::Policy';
  13         15  
  13         5798  
10              
11             our $VERSION = '0.5';
12              
13             Scalar my $EXPL => q{Avoid code that is nested, and thus difficult to grasp.};
14             Readonly my %BOOLEAN_OPS => map { $_ => 1 } qw( && || and or );
15              
16             sub supported_parameters {
17                 return ( {
18 12     12 0 79680             name => 'warn_level',
19                         description => 'The complexity score allowed before warning starts.',
20                         default_string => '10',
21                         behavior => 'integer',
22                         integer_minimum => 1,
23                     },
24                     {
25                         name => 'info_level',
26                         description => 'The complexity score allowed before informational reporting starts.',
27                         default_string => '1',
28                         behavior => 'integer',
29                         integer_minimum => 1,
30                     }
31                 );
32             }
33              
34             sub default_severity {
35 1     1 1 12     return $SEVERITY_MEDIUM;
36             }
37              
38             sub default_themes {
39 0     0 1 0     return qw( complexity maintenance );
40             }
41              
42             sub applies_to {
43 12     12 1 99389     return 'PPI::Statement::Sub';
44             }
45              
46             sub violates {
47 12     12 1 234     my ( $self, $elem, undef ) = @_;
48              
49             # only report complexity for named subs.
50 12 50       54     my $name = $elem->name() or return;
51              
52             # start with complexity of 0
53 12         252     my $score = 0;
54 12         70     my $block = $elem->find_first('PPI::Structure::Block');
55              
56 12         2826     $score += $self->_structure_score($block , 0);
57 12         51     $score += $self->_operator_score($block);
58 12         45     $score += $self->_recursion_score($block, $name);
59              
60             # return no violation
61 12 100       39     return if($score < $self->{'_info_level'});
62             # return violation
63 10         31     return ($self->_new_violation($elem, $score));
64             }
65              
66             sub _new_violation {
67 10     10   17     my $self = shift;
68 10         15     my ($elem, $score) = @_;
69 10         30     my $name = $elem->name();
70 10         172     my $desc = qq<Subroutine '$name' with complexity score of '$score'>;
71              
72                 return Perl::Critic::Violation->new( $desc, $EXPL, $elem,
73 10 100       116         ($score >= $self->{'_warn_level'} ? $self->get_severity() : $SEVERITY_LOWEST ));
74             }
75              
76             sub _structure_score {
77 284     284   210     my $self = shift;
78 284         213     my ( $elem, $nesting ) = @_;
79              
80 284 100       1040     return 0 unless ( $elem->can('schildren') );
81              
82 118         111     my $complexity = 0;
83              
84 118         168     for my $child ( $elem->schildren() ) {
85             #my $inc = 0;
86 272 100 100     2673         if ( $child->isa('PPI::Structure::Given')
    100 100        
      100        
      100        
87                         || $child->isa('PPI::Structure::Condition')
88                         || $child->isa('PPI::Structure::For')
89                         || $self->_is_foreach_statement($child)
90                         )
91                     {
92 15 100       96             if($self->_nesting_increase($child->parent)) {
93 11         15                 $complexity += $nesting;
94                         } else {
95             # missing compound statement / increment on postfix operator_score
96 4         5                 $complexity += $nesting + 1;
97                         }
98                     }
99             # 'return' is a break-statement, but does not count in terms of cognitive complexity.
100                     elsif ( $child->isa('PPI::Statement::Break') && ! $self->_is_return_statement($child)) {
101 1         10             $complexity++;
102                     }
103 272         545         $complexity += $self->_structure_score( $child, $nesting + $self->_nesting_increase($child) );
104                 }
105 118         166     return $complexity;
106             }
107              
108             sub _operator_score {
109 12     12   47     my $self = shift;
110 12         19     my ($sub) = @_;
111 12         17     my $by_parent = {};
112 12         58     my $elems = $sub->find('PPI::Token::Operator');
113 12         11555     my $sum = 0;
114 12 100       43     if($elems) {
115 7         153         map { push @{$by_parent->{$_->parent}}, $_->content }
  7         17  
116 9         33             grep { exists $BOOLEAN_OPS{$_->content} } @$elems;
  31         162  
117 9         138         for my $parent (keys %{$by_parent}) {
  9         30  
118 3         4             my @ops = @{$by_parent->{$parent}};
  3         10  
119 3         12             OP: for(my $i = 0; $i < scalar @ops; ++$i) {
120 7 100 100     25                 if($i > 0 && $ops[$i-1] eq $ops[$i]) {
121 2         7                     next OP;
122                             }
123 5         14                 $sum++;
124                         }
125                     }
126                 }
127 12         32     return $sum;
128             }
129              
130             sub _recursion_score {
131 12     12   16     my $self = shift;
132 12         23     my ($sub, $method_name) = @_;
133 12 100       58     if($sub->find(sub {
134             # TODO: check for false positives..
135 598 100   598   4415         $_[1]->isa( 'PPI::Token::Word' ) && $_[1]->content eq $method_name
136                 })) {
137 1         9         return 1;
138                 }
139 11         119     return 0;
140             }
141              
142             sub _is_return_statement {
143 9     9   13     my $self = shift;
144 9         10     my ($child) = @_;
145 9     34   40     scalar $child->find( sub { $_[1]->content eq 'return' });
  34         260  
146             }
147              
148             sub _is_foreach_statement {
149 258     258   268     my $self = shift;
150 258         180     my ($child) = @_;
151 258         571     my $foreach = $child->parent()->schild(0);
152 258   100     3490     return($child->isa('PPI::Structure::List') && $foreach && $foreach->isa('PPI::Token::Word') && $foreach->content eq 'foreach');
153             }
154              
155             sub _nesting_increase {
156 287     287   226     my $self = shift;
157 287         231     my ($child) = @_;
158              
159             # if/when/for...
160 287 100       627     return 1 if ($child->isa('PPI::Statement::Compound'));
161 267 100       562     return 1 if ($child->isa('PPI::Statement::Given'));
162             # anonymous sub
163 265 100 100 498   713     return 1 if ($child->isa('PPI::Statement') && $child->find( sub { $_[1]->content eq 'sub' }));
  498         4311  
164              
165 264         959     return 0;
166             }
167              
168             1;
169              
170             __END__
171            
172             =pod
173            
174             =head1 NAME
175            
176             Perl::Critic::Policy::CognitiveComplexity::ProhibitExcessCognitiveComplexity - Avoid code that is nested, and thus difficult to grasp.
177            
178             =head1 DESCRIPTION
179            
180             Cyclomatic Complexity was initially formulated as a measurement of the "testability and
181             maintainability" of the control flow of a module. While it excels at measuring the former, its
182             underlying mathematical model is unsatisfactory at producing a value that measures the
183             latter. A white paper from SonarSource* describes a new metric that breaks from the use of mathematical
184             models to evaluate code in order to remedy Cyclomatic Complexity's shortcomings and
185             produce a measurement that more accurately reflects the relative difficulty of understanding,
186             and therefore of maintaining methods, classes, and applications.
187            
188             * https://blog.sonarsource.com/cognitive-complexity-because-testability-understandability/
189            
190             =head2 Basic criteria and methodology
191            
192             A Cognitive Complexity score is assessed according to three basic rules:
193            
194             1. Ignore structures that allow multiple statements to be readably shorthanded into one
195             2. Increment (add one) for each break in the linear flow of the code
196             3. Increment when flow-breaking structures are nested
197            
198             Additionally, a complexity score is made up of three different types of increments:
199            
200             A. Nesting - assessed for nesting control flow structures inside each other
201             B. Structural - assessed on control flow structures that are subject to a nesting
202             increment
203             C. Fundamental - assessed on statements not subject to a nesting increment
204            
205             While the type of an increment makes no difference in the math - each increment adds one
206             to the final score - making a distinction among the categories of features being counted
207             makes it easier to understand where nesting increments do and do not apply.
208            
209            
210             =head1 EXAMPLES
211            
212             Some examples from the whitepaper, translated to perl.
213            
214             # Cyclomatic Complexity Cognitive Complexity
215            
216             Most simple case: subs themselves do not increment the cognitive complexity.
217            
218             sub a { # +1
219             } # =1 =0
220            
221             C<given/when> increments cognitive complexity only once.
222            
223             sub getWords { # +1
224             my ($number) = @_;
225             given ($number) { # +1
226             when (1) # +1
227             { return "one"; }
228             when (2) # +1
229             { return "a couple"; }
230             default # +1
231             { return "lots"; }
232             }
233             } # =4 =1
234            
235             The deeper the nesting, the more control-structures add to the complexity.
236            
237             C<goto>, C<next> and C<last> break the linear flow, which increments the
238             complexity by one.
239            
240             sub sumOfPrimes {
241             my ($max) = @_;
242             my $total = 0;
243             OUT: for (my $i = 1; $i <= $max; ++$i) { # +1
244             for (my $j = 2; $j < $i; ++$j) { # +2
245             if ($i % $j == 0) { # +3
246             next OUT; # +1
247             }
248             }
249             $total += $i;
250             }
251             return $total;
252             } # =7
253            
254             Anonymous functions do not increment the complexity, but the nesting.
255            
256             sub closure {
257             sub { # +0 (nesting=1)
258             if(1) { # +2 (nesting=1)
259             return; +0 (nesting=2)
260             }
261             }->();
262             } =2
263            
264             Cognitive Complexity does not increment for each logical operator.
265             Instead, it assesses a fundamental increment for each sequence of logical operators.
266            
267             sub boolMethod2 {
268             if( # +1
269             $a && $b && $c # +1
270             || # +1
271             $d && $e) # +1
272             {
273             } # =4
274            
275