File Coverage

lib/Code/Statistics/Reporter.pm
Criterion Covered Total %
statement 171 172 99.4
branch 13 18 72.2
condition 6 10 60.0
subroutine 31 31 100.0
pod 1 1 100.0
total 222 232 95.6


line stmt bran cond sub pod time code
1 1     1   7 use strict;
  1         1  
  1         31  
2 1     1   5 use warnings;
  1         1  
  1         45  
3              
4             package Code::Statistics::Reporter;
5             $Code::Statistics::Reporter::VERSION = '1.190680';
6             # ABSTRACT: creates reports statistics and outputs them
7              
8 1     1   29 use 5.004;
  1         4  
9              
10 1     1   5 use Moose;
  1         2  
  1         6  
11 1     1   5805 use MooseX::HasDefaults::RO;
  1         2  
  1         7  
12 1     1   4523 use Code::Statistics::MooseTypes;
  1         2  
  1         22  
13 1     1   4 use Code::Statistics::Metric;
  1         2  
  1         26  
14              
15 1     1   13 use Carp 'confess';
  1         2  
  1         69  
16 1     1   5 use JSON 'from_json';
  1         2  
  1         9  
17 1     1   118 use File::Slurp 'read_file';
  1         2  
  1         44  
18 1     1   5 use List::Util qw( reduce max sum min );
  1         1  
  1         64  
19 1     1   605 use Data::Section -setup;
  1         10468  
  1         7  
20 1     1   1077 use Template;
  1         15927  
  1         39  
21 1     1   9 use List::MoreUtils qw( uniq );
  1         1  
  1         14  
22 1     1   644 use Clone qw( clone );
  1         3  
  1         1537  
23              
24             has quiet => ( isa => 'Bool' );
25              
26             has file_ignore => (
27             isa => 'CS::InputList',
28             coerce => 1,
29             default => sub {[]},
30             );
31              
32             has screen_width => ( isa => 'Int', default => 80 );
33             has min_path_width => ( isa => 'Int', default => 12 );
34             has table_length => ( isa => 'Int', default => 10 );
35              
36              
37             sub report {
38 1     1 1 1588 my ( $self ) = @_;
39              
40 1         7 my $stats = from_json read_file('codestat.out');
41              
42 1         252 $stats->{files} = $self->_strip_ignored_files( @{ $stats->{files} } );
  1         4  
43 1         5 $stats->{target_types} = $self->_prepare_target_types( $stats->{files} );
44              
45 1         2 $_->{metrics} = $self->_process_target_type( $_, $stats->{metrics} ) for @{$stats->{target_types}};
  1         5  
46              
47 1         2 my $output;
48 1         6 my $tmpl = $self->section_data( 'dos_template' );
49 1         2645 my $tt = Template->new( STRICT => 1 );
50             $tt->process(
51             $tmpl,
52             {
53             targets => $stats->{target_types},
54             truncate_front => sub {
55 128     128   163070 my ( $string, $length ) = @_;
56 128 50       393 return $string if $length >= length $string;
57 0         0 return substr $string, 0-$length, $length;
58             },
59             },
60 1 50       21700 \$output
61             ) or confess $tt->error;
62              
63 1 50       1174 print $output if !$self->quiet;
64              
65 1         18 return $output;
66             }
67              
68             sub _strip_ignored_files {
69 1     1   4 my ( $self, @files ) = @_;
70              
71 1         3 my @ignore_regexes = grep { $_ } @{ $self->file_ignore };
  2         6  
  1         27  
72              
73 1         3 for my $re ( @ignore_regexes ) {
74 1         2 @files = grep { $_->{path} !~ $re } @files;
  4         22  
75             }
76              
77 1         3 return \@files;
78             }
79              
80             sub _sort_columns {
81 10     10   27 my ( $self, %widths ) = @_;
82              
83             # get all columns in the right order
84 10         19 my @start_columns = qw( path line col );
85 10         14 my %end_columns = ( 'deviation' => 1 );
86 10         41 my @columns = uniq grep { !$end_columns{$_} } @start_columns, sort keys %widths;
  110         171  
87 10         29 push @columns, keys %end_columns;
88              
89 10         13 @columns = grep { $widths{$_} } @columns; # remove the ones that have no data
  80         85  
90              
91             # expand the rest
92 10         22 @columns = map $self->_make_col_hash( $_, \%widths ), @columns;
93              
94             # calculate the width left over for the first column
95 10         33 my $used_width = sum( values %widths ) - $columns[0]{width};
96 10         240 my $first_col_width = $self->screen_width - $used_width;
97              
98             # special treatment for the first column
99 10         21 for ( @columns[0..0] ) {
100 10         181 $_->{width} = max( $self->min_path_width, $first_col_width );
101 10         22 $_->{printname} = substr $_->{printname}, 1;
102             }
103              
104 10         31 return \@columns;
105             }
106              
107             sub _make_col_hash {
108 80     80   97 my ( $self, $col, $widths ) = @_;
109              
110 80         88 my $short_name = $self->_col_short_name($_);
111             my $col_hash = {
112             name => $_,
113 80         177 width => $widths->{$_},
114             printname => " $short_name",
115             };
116              
117 80         149 return $col_hash;
118             }
119              
120             sub _prepare_target_types {
121 1     1   3 my ( $self, $files ) = @_;
122              
123 1         1 my %target_types;
124              
125 1         2 for my $file ( @{$files} ) {
  1         3  
126 3         5 for my $target_type ( keys %{$file->{measurements}} ) {
  3         5  
127 7         7 for my $target ( @{$file->{measurements}{$target_type}} ) {
  7         11  
128 37         50 $target->{path} = $file->{path};
129 37         35 push @{ $target_types{$target_type}->{list} }, $target;
  37         47  
130             }
131             }
132             }
133              
134 1         6 $target_types{$_}->{type} = $_ for keys %target_types;
135              
136 1         2 my $i = 0;
137 1         8 my %pref = map +( $_ => ++$i ), reverse qw( RootDocument Block Sub nop );
138 1   50     7 my @types = reverse sort { ( $pref{$a} || 0 ) <=> ( $pref{$b} || 0 ) } reverse sort keys %target_types;
  3   50     10  
139              
140 1         6 return [ map $target_types{$_}, @types ];
141             }
142              
143             sub _process_target_type {
144 3     3   7 my ( $self, $target_type, $metrics ) = @_;
145              
146 3         4 my @metric = map $self->_process_metric( $target_type, $_ ), @{$metrics};
  3         8  
147              
148 3         12 return \@metric;
149             }
150              
151             sub _process_metric {
152 24     24   31 my ( $self, $target_type, $metric ) = @_;
153              
154 24 100       87 return if "Code::Statistics::Metric::$metric"->is_insignificant;
155 12 50 33     27 return if !$target_type->{list} or !@{$target_type->{list}};
  12         32  
156 12 50       27 return if !exists $target_type->{list}[0]{$metric};
157              
158 12         12 my @list = reverse sort { $a->{$metric} <=> $b->{$metric} } @{$target_type->{list}};
  373         378  
  12         33  
159              
160 12         33 my $metric_data = { type => $metric };
161              
162 12         26 $metric_data->{avg} = $self->_calc_average( $metric, @list );
163              
164 12 100 100     50 $self->_prepare_metric_tables( $metric_data, @list ) if $metric_data->{avg} and $metric_data->{avg} != 1;
165              
166 12         32 return $metric_data;
167             }
168              
169             sub _prepare_metric_tables {
170 10     10   18 my ( $self, $metric_data, @list ) = @_;
171              
172 10         24 $metric_data->{top} = $self->_get_top( @list );
173 10         32 $metric_data->{bottom} = $self->_get_bottom( @list );
174 10         16 $self->_calc_deviation( $_, $metric_data ) for ( @{$metric_data->{top}}, @{$metric_data->{bottom}} );
  10         18  
  10         25  
175 10         18 $metric_data->{widths} = $self->_calc_widths( $metric_data );
176 10         13 $metric_data->{columns} = $self->_sort_columns( %{ $metric_data->{widths} } );
  10         27  
177              
178 10         22 return;
179             }
180              
181             sub _calc_deviation {
182 128     128   179 my ( $self, $line, $metric_data ) = @_;
183              
184 128         122 my $avg = $metric_data->{avg};
185 128         116 my $type = $metric_data->{type};
186              
187 128         139 my $deviation = $line->{$type} / $avg;
188 128         399 $line->{deviation} = sprintf '%.2f', $deviation;
189              
190 128         212 return;
191             }
192              
193             sub _calc_widths {
194 10     10   19 my ( $self, $metric_data ) = @_;
195              
196 10         11 my @entries = @{$metric_data->{top}};
  10         18  
197 10         13 @entries = ( @entries, @{$metric_data->{bottom}} );
  10         20  
198              
199 10         11 my @columns = keys %{$entries[0]};
  10         26  
200              
201 10         12 my %widths;
202 10         12 for my $col ( @columns ) {
203 80         85 my @lengths = map { length $_->{$col} } @entries;
  1024         1213  
204 80         129 push @lengths, length $self->_col_short_name($col);
205 80         137 my $max = max @lengths;
206 80         136 $widths{$col} = $max;
207             }
208              
209 10         30 $_++ for values %widths;
210              
211 10         26 return \%widths;
212             }
213              
214             sub _calc_average {
215 12     12   22 my ( $self, $metric, @list ) = @_;
216              
217 12     148   72 my $sum = reduce { $a + $b->{$metric} } 0, @list;
  148         143  
218 12         36 my $average = $sum / @list;
219              
220 12         24 return $average;
221             }
222              
223             sub _get_top {
224 10     10   19 my ( $self, @list ) = @_;
225              
226 10         239 my $slice_end = min( $#list, $self->table_length - 1 );
227 10         23 my @top = grep { defined } @list[ 0 .. $slice_end ];
  79         94  
228              
229 10         668 return clone \@top;
230             }
231              
232             sub _get_bottom {
233 10     10   19 my ( $self, @list ) = @_;
234              
235 10 100       225 return [] if @list < $self->table_length;
236              
237 7         10 @list = reverse @list;
238 7         127 my $slice_end = min( $#list, $self->table_length - 1 );
239 7         18 my @bottom = @list[ 0 .. $slice_end ];
240              
241 7         134 my $bottom_size = @list - $self->table_length;
242 7 100       119 @bottom = splice @bottom, 0, $bottom_size if $bottom_size < $self->table_length;
243              
244 7         339 return clone \@bottom;
245             }
246              
247             sub _col_short_name {
248 160     160   167 my ( $self, $col ) = @_;
249 160         391 return ucfirst "Code::Statistics::Metric::$col"->short_name;
250             }
251              
252             1;
253              
254             =pod
255              
256             =encoding UTF-8
257              
258             =head1 NAME
259              
260             Code::Statistics::Reporter - creates reports statistics and outputs them
261              
262             =head1 VERSION
263              
264             version 1.190680
265              
266             =head2 reports
267             Creates a report on given code statistics and outputs it in some way.
268              
269             =head1 AUTHOR
270              
271             Christian Walde <mithaldu@yahoo.de>
272              
273             =head1 COPYRIGHT AND LICENSE
274              
275              
276             Christian Walde has dedicated the work to the Commons by waiving all of his
277             or her rights to the work worldwide under copyright law and all related or
278             neighboring legal rights he or she had in the work, to the extent allowable by
279             law.
280              
281             Works under CC0 do not require attribution. When citing the work, you should
282             not imply endorsement by the author.
283              
284             =cut
285              
286             __DATA__
287             __[ dos_template ]__
288              
289             ================================================================================
290             ============================ Code Statistics Report ============================
291             ================================================================================
292              
293             [% FOR target IN targets %]
294             ================================================================================
295              
296             [%- " " FILTER repeat( ( 80 - target.type.length ) / 2 ) %][% target.type %]
297             ================================================================================
298              
299              
300             [%- "averages" %]
301              
302             [%- FOR metric IN target.metrics %]
303             [%- metric.type %]: [% metric.avg %]
304              
305             [%- END %]
306              
307             [%- FOR metric IN target.metrics %]
308             [%- NEXT IF !metric.defined( 'top' ) and !metric.defined( 'bottom' ) %]
309              
310             [%- " " FILTER repeat( ( 80 - metric.type.length ) / 2 ) %][% metric.type %]
311              
312             [%- FOR table_mode IN [ 'top', 'bottom' ] %]
313             [%- NEXT IF !metric.$table_mode.size -%]
314             [%- table_mode %] ten
315              
316             [%- FOR column IN metric.columns -%]
317             [%- column.printname FILTER format("%-${column.width}s") -%]
318             [%- END %]
319             --------------------------------------------------------------------------------
320              
321             [%- FOR line IN metric.$table_mode -%]
322             [%- FOR column IN metric.columns -%]
323             [%- IF column.name == 'path' # align to the left and truncate -%]
324             [%- truncate_front( line.${column.name}, column.width ) FILTER format("%-${column.width}s") -%]
325             [%- ELSE # align to the right -%]
326             [%- line.${column.name} FILTER format("%${column.width}s") -%]
327             [%- END -%]
328             [%- END %]
329              
330             [%- END -%]
331             --------------------------------------------------------------------------------
332              
333              
334             [%- END %]
335             [%- END -%]
336             [%- END -%]