File Coverage

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


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