File Coverage

blib/lib/Perl/Metrics/Simple/Analysis/File.pm
Criterion Covered Total %
statement 207 223 92.8
branch 44 62 70.9
condition 4 5 80.0
subroutine 28 31 90.3
pod 15 15 100.0
total 298 336 88.6


line stmt bran cond sub pod time code
1             package Perl::Metrics::Simple::Analysis::File;
2 7     7   138400 use strict;
  7         25  
  7         254  
3 7     7   41 use warnings;
  7         14  
  7         206  
4              
5 7     7   37 use Carp qw(cluck confess);
  7         14  
  7         413  
6 7     7   54 use Data::Dumper;
  7         16  
  7         358  
7 7     7   48 use English qw(-no_match_vars);
  7         25  
  7         57  
8 7     7   3229 use Perl::Metrics::Simple::Analysis;
  7         21  
  7         271  
9 7     7   41 use PPI 1.113;
  7         139  
  7         203  
10 7     7   44 use PPI::Document;
  7         31  
  7         228  
11 7     7   37 use Readonly;
  7         12  
  7         19594  
12              
13             our $VERSION = 'v1.0.3';
14              
15             Readonly::Scalar my $ALL_NEWLINES_REGEX =>
16             qr/ ( \Q$INPUT_RECORD_SEPARATOR\E ) /sxm;
17             Readonly::Array our @DEFAULT_LOGIC_OPERATORS => qw(
18             !
19             !~
20             &&
21             &&=
22             //
23             <
24             <<=
25             <=>
26             ==
27             =~
28             >
29             >>=
30             ?
31             and
32             cmp
33             eq
34             gt
35             lt
36             ne
37             not
38             or
39             xor
40             ||
41             ||=
42             ~~
43             );
44              
45             Readonly::Array our @DEFAULT_LOGIC_KEYWORDS => qw(
46             else
47             elsif
48             for
49             foreach
50             goto
51             grep
52             if
53             last
54             map
55             next
56             unless
57             until
58             while
59             );
60              
61             Readonly::Array our @DEFAULT_METHOD_MODIFIERS => qw(
62             before
63             after
64             around
65             );
66              
67             Readonly::Scalar my $LAST_CHARACTER => -1;
68              
69             Readonly::Scalar my $ONE_SPACE => q{ };
70              
71             Readonly::Scalar my $PPI_CHILD_INDEX_AFTER => 1;
72             Readonly::Scalar my $PPI_CHILD_INDEX_METHOD_NAME => 2;
73             Readonly::Scalar my $PPI_CHILD_INDEX_OPERATOR => 3;
74             Readonly::Scalar my $PPI_CHILD_INDEX_SUBROUTINE => 4;
75             Readonly::Scalar my $PPI_CHILD_INDEX_BLOCK => 5;
76              
77             our (@LOGIC_KEYWORDS, @LOGIC_OPERATORS, @METHOD_MODIFIERS); # For user-supplied values;
78              
79             our (%LOGIC_KEYWORDS, %LOGIC_OPERATORS, %METHOD_MODIFIERS); # Populated in _init()
80              
81             # Private instance variables:
82             my %_PATH = ();
83             my %_MAIN_STATS = ();
84             my %_SUBS = ();
85             my %_PACKAGES = ();
86             my %_LINES = ();
87             my %_LOGIC_KEYWORDS = ();
88             my %_LOGIC_OPERATORS = ();
89             my %_METHOD_MODIFIERS = ();
90              
91             sub new {
92 41     41 1 6844 my ( $class, %parameters ) = @_;
93 41         105 my $self = {};
94 41         113 bless $self, $class;
95 41         183 $self->_init(%parameters);
96 41         23483 return $self;
97             }
98              
99             sub _init {
100 41     41   114 my ( $self, %parameters ) = @_;
101 41         185 $_PATH{$self} = $parameters{'path'};
102              
103 41         123 my $path = $self->path();
104              
105 41         79 my $document;
106 41 100       134 if (ref $path) {
107 9 50       25 if (ref $path eq 'SCALAR') {
108 9         53 $document = PPI::Document->new($path);
109             } else {
110 0         0 $document = $path;
111             }
112             } else {
113 32 50       1095 if ( !-r $path ) {
114 0         0 Carp::confess "Path '$path' is missing or not readable!";
115             }
116 32         174 $document = _create_ppi_document($path);
117             }
118              
119 41 100       76444 my @logic_keywords = @LOGIC_KEYWORDS ? @LOGIC_KEYWORDS : @DEFAULT_LOGIC_KEYWORDS;
120 41         2572 %LOGIC_KEYWORDS = hashify(@logic_keywords);
121 41         189 $_LOGIC_OPERATORS{$self} = \%LOGIC_KEYWORDS;
122              
123 41 100       210 my @logic_operators = @LOGIC_OPERATORS ? @LOGIC_OPERATORS : @DEFAULT_LOGIC_OPERATORS;
124 41         4212 %LOGIC_OPERATORS = hashify(@logic_operators);
125 41         161 $_LOGIC_OPERATORS{$self} = \%LOGIC_OPERATORS;
126              
127 41 50       242 my @method_modifiers = @METHOD_MODIFIERS ? @METHOD_MODIFIERS : @DEFAULT_METHOD_MODIFIERS;
128 41         775 %METHOD_MODIFIERS = hashify(@method_modifiers);
129 41         210 $_METHOD_MODIFIERS{$self} = \%METHOD_MODIFIERS;
130              
131 41         163 $document = $self->_make_pruned_document($document);
132              
133 41 50       141 if ( !defined $document ) {
134 0         0 cluck "Could not make a PPI document from '$path'";
135 0         0 return;
136             }
137              
138 41         152 my $packages = _get_packages($document);
139              
140 41         103 my @sub_analysis = ();
141 41         126 my $sub_elements = $document->find('PPI::Statement::Sub');
142 41         93554 @sub_analysis = @{ $self->_iterate_over_subs($sub_elements) };
  41         153  
143              
144 41         195 $_MAIN_STATS{$self}
145             = $self->analyze_main( $document, $sub_elements, \@sub_analysis );
146 41         11253 $_SUBS{$self} = \@sub_analysis;
147 41         120 $_PACKAGES{$self} = $packages;
148 41         114 $_LINES{$self} = $self->get_node_length($document);
149              
150 41         474 return $self;
151             }
152              
153             sub _create_ppi_document {
154 32     32   80 my $path = shift;
155 32         55 my $document;
156 32 100       438 if ( -s $path ) {
157 29         211 $document = PPI::Document->new($path);
158             }
159             else {
160              
161             # The file is empty. Create a PPI document with a single whitespace
162             # chararacter. This makes sure that the PPI tokens() method
163             # returns something, so we avoid a warning from
164             # PPI::Document::index_locations() which expects tokens() to return
165             # something other than undef.
166 3         11 my $one_whitespace_character = q{ };
167 3         20 $document = PPI::Document->new( \$one_whitespace_character );
168             }
169 32         383676 return $document;
170             }
171              
172             sub _make_pruned_document {
173 41     41   113 my ($self, $document) = @_;
174 41         116 $document = _prune_non_code_lines($document);
175 41         240 $document = $self->_rewrite_moose_method_modifiers($document);
176 41         407 $document->index_locations();
177 41         96621 $document->readonly(1);
178 41         167 return $document;
179             }
180              
181             sub all_counts {
182 5     5 1 40 my $self = shift;
183 5         18 my $stats_hash = {
184             path => $self->path,
185             lines => $self->lines,
186             main_stats => $self->main_stats,
187             subs => $self->subs,
188             packages => $self->packages,
189             };
190 5         38 return $stats_hash;
191             }
192              
193             sub analyze_main {
194 41     41 1 118 my $self = shift;
195 41         76 my $document = shift;
196 41         72 my $sub_elements = shift;
197 41         78 my $sub_analysis = shift;
198              
199 41 50       153 if ( !$document->isa('PPI::Document') ) {
200 0         0 Carp::confess('Did not supply a PPI::Document');
201             }
202              
203 41         127 my $lines = $self->get_node_length($document);
204 41         81 foreach my $sub ( @{$sub_analysis} ) {
  41         139  
205 43         110 $lines -= $sub->{lines};
206             }
207 41         164 my $document_without_subs = $document->clone;
208 41         55759 $document_without_subs->prune('PPI::Statement::Sub');
209 41         63553 my $complexity = $self->measure_complexity($document_without_subs);
210 41         201 my $results = {
211             name => '{code not in named subroutines}',
212             lines => $lines,
213             mccabe_complexity => $complexity,
214             path => $self->path,
215             };
216 41         181 return $results;
217             }
218              
219             sub get_node_length {
220 218     218 1 7101 my ( $self, $node ) = @_;
221 218         366 my $eval_result = eval { $node = _prune_non_code_lines($node); };
  218         440  
222 218 50       942 return 0 if not $eval_result;
223 218 50       543 return 0 if ( !defined $node );
224 218         615 my $string = $node->content;
225 218 100       48069 return 0 if ( !length $string );
226              
227             # Replace whitespace-newline with newline
228 216         2838 $string =~ s/ \s+ \Q$INPUT_RECORD_SEPARATOR\E /$INPUT_RECORD_SEPARATOR/smxg;
229 216         2151 $string =~ s/\Q$INPUT_RECORD_SEPARATOR\E /$INPUT_RECORD_SEPARATOR/smxg;
230 216         862 $string =~ s/ \A \s+ //msx; # Remove leading whitespace
231 216         1793 my @newlines = ( $string =~ /$ALL_NEWLINES_REGEX/smxg );
232 216         475 my $line_count = scalar @newlines;
233              
234             # if the string is not empty and the last character is not a newline then add 1
235 216 100       585 if ( length $string ) {
236 202         448 my $last_char = substr $string, $LAST_CHARACTER, 1;
237 202 100       577 if ( $last_char ne "$INPUT_RECORD_SEPARATOR" ) {
238 99         200 $line_count++;
239             }
240             }
241              
242 216         948 return $line_count;
243             }
244              
245             sub path {
246 186     186 1 423 my ($self) = @_;
247 186         824 return $_PATH{$self};
248             }
249              
250             sub main_stats {
251 89     89 1 153 my ($self) = @_;
252 89         260 return $_MAIN_STATS{$self};
253             }
254              
255             sub subs {
256 37     37 1 82 my ($self) = @_;
257 37         130 return $_SUBS{$self};
258             }
259              
260             sub packages {
261 37     37 1 128 my ($self) = @_;
262 37         155 return $_PACKAGES{$self};
263             }
264              
265             sub lines {
266 33     33 1 78 my ($self) = @_;
267 33         106 return $_LINES{$self};
268             }
269              
270             sub logic_keywords {
271 0     0 1 0 my ($self) = @_;
272 0 0       0 return wantarray ? @{$_LOGIC_KEYWORDS{$self}} : $_LOGIC_KEYWORDS{$self};
  0         0  
273             }
274              
275             sub logic_operators {
276 0     0 1 0 my ($self) = @_;
277 0 0       0 return wantarray ? @{$_LOGIC_OPERATORS{$self}} : $_LOGIC_OPERATORS{$self};
  0         0  
278             }
279              
280             sub method_modifiers {
281 0     0 1 0 my ($self) = @_;
282 0 0       0 return wantarray ? @{$_METHOD_MODIFIERS{$self}} : $_METHOD_MODIFIERS{$self};
  0         0  
283             }
284              
285             sub measure_complexity {
286 91     91 1 16855 my $self = shift;
287 91         181 my $elem = shift;
288              
289 91         171 my $complexity_count = 0;
290 91 100       237 if ( $self->get_node_length($elem) == 0 ) {
291 10         34 return $complexity_count;
292             }
293              
294 81 50       296 if ($elem) {
295 81         181 $complexity_count++;
296             }
297              
298             # Count up all the logic keywords, weed out hash keys
299 81   50     270 my $keywords_ref = $elem->find('PPI::Token::Word') || [];
300 81         105075 my @filtered = grep { !is_hash_key($_) } @{$keywords_ref};
  622         1161  
  81         266  
301 81         256 $complexity_count += grep { exists $LOGIC_KEYWORDS{$_} } @filtered;
  604         2268  
302              
303             # Count up all the logic operators
304 81         501 my $operators_ref = $elem->find('PPI::Token::Operator');
305 81 100       103367 if ($operators_ref) {
306             $complexity_count
307 42         130 += grep { exists $LOGIC_OPERATORS{$_} } @{$operators_ref};
  152         653  
  42         111  
308             }
309 81         741 return $complexity_count;
310             }
311              
312             sub _get_packages {
313 41     41   90 my $document = shift;
314              
315 41         92 my @unique_packages = ();
316 41         167 my $found_packages = $document->find('PPI::Statement::Package');
317              
318             return \@unique_packages
319 41 100       95092 if (
320             !Perl::Metrics::Simple::Analysis::is_ref( $found_packages, 'ARRAY' ) );
321              
322 17         67 my %seen_packages = ();
323              
324 17         38 foreach my $package ( @{$found_packages} ) {
  17         52  
325 29         443 $seen_packages{ $package->namespace() }++;
326             }
327              
328 17         646 @unique_packages = sort keys %seen_packages;
329              
330 17         73 return \@unique_packages;
331             }
332              
333             sub _iterate_over_subs {
334 41     41   83 my $self = shift;
335 41         102 my $found_subs = shift;
336              
337 41 100       136 return []
338             if ( !Perl::Metrics::Simple::Analysis::is_ref( $found_subs, 'ARRAY' ) );
339              
340 21         80 my @subs = ();
341              
342 21         49 foreach my $sub ( @{$found_subs} ) {
  21         69  
343 43         141 my $sub_length = $self->get_node_length($sub);
344 43         150 push @subs,
345             {
346             path => $self->path,
347             name => $sub->name,
348             lines => $sub_length,
349             mccabe_complexity => $self->measure_complexity($sub),
350             };
351             }
352 21         137 return \@subs;
353             }
354              
355             #-------------------------------------------------------------------------
356             # Copied from
357             # http://search.cpan.org/src/THALJEF/Perl-Critic-0.19/lib/Perl/Critic/Utils.pm
358             sub hashify {
359 123     123 1 405 my @hash_keys = @_;
360 123         235 return map { $_ => 1 } @hash_keys;
  1684         3645  
361             }
362              
363             #-------------------------------------------------------------------------
364             # Copied and somehwat simplified from
365             # http://search.cpan.org/src/THALJEF/Perl-Critic-0.19/lib/Perl/Critic/Utils.pm
366             sub is_hash_key {
367 627     627 1 11075 my $ppi_elem = shift;
368              
369 627         903 my $is_hash_key = eval {
370 627         1351 my $parent = $ppi_elem->parent();
371 627         3008 my $grandparent = $parent->parent();
372 627 100       3032 if ( $grandparent->isa('PPI::Structure::Subscript') ) {
373 14         42 return 1;
374             }
375 613         1303 my $sib = $ppi_elem->snext_sibling();
376 613 100 100     15264 if ( $sib->isa('PPI::Token::Operator') && $sib eq '=>' ) {
377 6         160 return 1;
378             }
379 605         1657 return;
380             };
381              
382 627         1719 return $is_hash_key;
383             }
384              
385             sub _prune_non_code_lines {
386 259     259   446 my $document = shift;
387 259 50       635 if ( !defined $document ) {
388 0         0 Carp::confess('Did not supply a document!');
389             }
390 259         844 $document->prune('PPI::Token::Comment');
391 259         432059 $document->prune('PPI::Token::Pod');
392 259         420724 $document->prune('PPI::Token::End');
393              
394 259         420189 return $document;
395             }
396              
397             sub _rewrite_moose_method_modifiers {
398 41     41   141 my ($self, $document) = @_;
399 41 50       143 if ( !defined $document ) {
400 0         0 Carp::confess('Did not supply a document!');
401             }
402              
403 41         130 my $re = q{^(} . join(q{|}, map {quotemeta} keys %{$_METHOD_MODIFIERS{$self}}) . q{)$};
  123         405  
  41         221  
404             my @method_modifiers =
405             # 5th child: { ... }
406 8         77 grep { $_->[$PPI_CHILD_INDEX_BLOCK]->isa('PPI::Structure::Block') }
407              
408             # 4th child: sub
409             grep {
410 8 50       165 $_->[$PPI_CHILD_INDEX_SUBROUTINE]->isa('PPI::Token::Word')
411             && $_->[$PPI_CHILD_INDEX_SUBROUTINE]->content eq 'sub'
412             }
413              
414             # 3rd child: =>
415             grep {
416 8 50       60 $_->[$PPI_CHILD_INDEX_OPERATOR]->isa('PPI::Token::Operator')
417             && $_->[$PPI_CHILD_INDEX_OPERATOR]->content eq '=>'
418             }
419              
420             # 2nd child: 'method_name'
421 8 100       259 grep { $_->[$PPI_CHILD_INDEX_METHOD_NAME]->isa('PPI::Token::Quote')
422             || $_->[$PPI_CHILD_INDEX_METHOD_NAME]->isa('PPI::Token::Word') }
423              
424             # 1st child: after
425             grep {
426 70 100       1890 $_->[$PPI_CHILD_INDEX_AFTER]->isa('PPI::Token::Word')
427             && $_->[$PPI_CHILD_INDEX_AFTER]->content =~ /$re/smx
428             }
429              
430             # create an arrayref [item, child0, child1, child2]
431             # for easier, cheaper access
432 70         713 map { [ $_, $_->schildren ] }
433              
434             # don't want subclasses of PPI::Statement here
435 41         216 grep { $_->class eq 'PPI::Statement' } $document->schildren;
  215         1985  
436              
437 41         493 for (@method_modifiers) {
438 8         14 my ($old_stmt, @children) = @{$_};
  8         66  
439 8         42 my $name = '_' . $children[0]->literal . '_';
440 8 100       195 if ( $children[1]->can('literal') ) {
441 6         30 $name .= $children[1]->literal;
442             }
443             else {
444 2         12 my $string = $children[1]->string;
445 2         28 $name .= $string;
446             }
447 8         154 my $new_stmt = PPI::Statement::Sub->new();
448 8         123 $new_stmt->add_element(PPI::Token::Word->new('sub'));
449 8         355 $new_stmt->add_element(PPI::Token::Whitespace->new($ONE_SPACE));
450 8         164 $new_stmt->add_element(PPI::Token::Word->new($name));
451 8         150 $new_stmt->add_element(PPI::Token::Whitespace->new($ONE_SPACE));
452 8         181 $new_stmt->add_element($children[$PPI_CHILD_INDEX_SUBROUTINE]->clone());
453              
454 8         1174 $old_stmt->insert_after($new_stmt);
455 8         527 $old_stmt->delete();
456             }
457              
458 41         1479 return $document;
459             }
460              
461             1;
462              
463             __END__